1.1 --- a/convert.py Tue Jul 24 15:53:44 2018 +0200
1.2 +++ b/convert.py Tue Jul 24 23:37:44 2018 +0200
1.3 @@ -1,16 +1,9 @@
1.4 #!/usr/bin/env python
1.5
1.6 -from moinformat import all_parsers, get_serialiser, parse, serialise
1.7 +from moinformat import get_serialiser, make_parser, parse, serialise
1.8 from os.path import split
1.9 import sys
1.10
1.11 -def test_option(args, name):
1.12 - if name in args:
1.13 - args.remove(name)
1.14 - return True
1.15 - else:
1.16 - return False
1.17 -
1.18 def main():
1.19 dirname, progname = split(sys.argv[0])
1.20 args = sys.argv[1:]
1.21 @@ -18,6 +11,7 @@
1.22 l = filenames = []
1.23 formats = []
1.24 tree = False
1.25 + macros = False
1.26
1.27 for arg in args:
1.28
1.29 @@ -26,6 +20,11 @@
1.30 if arg == "--tree":
1.31 tree = True
1.32
1.33 + # Detect macro evaluation.
1.34 +
1.35 + elif arg == "--macros":
1.36 + macros = True
1.37 +
1.38 # Switch to collecting formats
1.39
1.40 elif arg == "--format":
1.41 @@ -45,7 +44,12 @@
1.42
1.43 f = open(filename)
1.44 try:
1.45 - d = parse(f.read(), all_parsers)
1.46 + p = make_parser()
1.47 + d = parse(f.read(), p)
1.48 +
1.49 + if macros:
1.50 + p.evaluate_macros()
1.51 +
1.52 if tree:
1.53 print d.prettyprint()
1.54 else:
2.1 --- a/moinformat/__init__.py Tue Jul 24 15:53:44 2018 +0200
2.2 +++ b/moinformat/__init__.py Tue Jul 24 23:37:44 2018 +0200
2.3 @@ -22,9 +22,21 @@
2.4 from moinformat.parsers import parse, parsers as all_parsers
2.5 from moinformat.serialisers import serialise, serialisers as all_serialisers
2.6
2.7 +def get_parser(name="moin"):
2.8 +
2.9 + "Return the parser class supporting the format with the given 'name'."
2.10 +
2.11 + return all_parsers[name]
2.12 +
2.13 +def make_parser(name="moin"):
2.14 +
2.15 + "Return a parser instance for the format with the given 'name'."
2.16 +
2.17 + return get_parser(name)(all_parsers)
2.18 +
2.19 def get_serialiser(name):
2.20
2.21 - "Return the main serialiser for the format having the given 'name'."
2.22 + "Return the main serialiser class for the format having the given 'name'."
2.23
2.24 return all_serialisers["%s.moin" % name]
2.25
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/moinformat/macros/__init__.py Tue Jul 24 23:37:44 2018 +0200
3.3 @@ -0,0 +1,32 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +Moin macro implementations.
3.8 +
3.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This program is free software; you can redistribute it and/or modify it under
3.12 +the terms of the GNU General Public License as published by the Free Software
3.13 +Foundation; either version 3 of the License, or (at your option) any later
3.14 +version.
3.15 +
3.16 +This program is distributed in the hope that it will be useful, but WITHOUT
3.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
3.19 +details.
3.20 +
3.21 +You should have received a copy of the GNU General Public License along with
3.22 +this program. If not, see <http://www.gnu.org/licenses/>.
3.23 +"""
3.24 +
3.25 +from moinformat.macros.manifest import macros
3.26 +
3.27 +# Top-level functions.
3.28 +
3.29 +def get_macro(name):
3.30 +
3.31 + "Return the macro with the given 'name' or None if no macro is found."
3.32 +
3.33 + return macros.get(name)
3.34 +
3.35 +# vim: tabstop=4 expandtab shiftwidth=4
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/moinformat/macros/common.py Tue Jul 24 23:37:44 2018 +0200
4.3 @@ -0,0 +1,33 @@
4.4 +#!/usr/bin/env python
4.5 +
4.6 +"""
4.7 +Common macro functionality.
4.8 +
4.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
4.10 +
4.11 +This program is free software; you can redistribute it and/or modify it under
4.12 +the terms of the GNU General Public License as published by the Free Software
4.13 +Foundation; either version 3 of the License, or (at your option) any later
4.14 +version.
4.15 +
4.16 +This program is distributed in the hope that it will be useful, but WITHOUT
4.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
4.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
4.19 +details.
4.20 +
4.21 +You should have received a copy of the GNU General Public License along with
4.22 +this program. If not, see <http://www.gnu.org/licenses/>.
4.23 +"""
4.24 +
4.25 +class Macro:
4.26 +
4.27 + "Common macro functionality."
4.28 +
4.29 + def __init__(self, node, doc):
4.30 +
4.31 + "Initialise the macro with its tree 'node' and document root, 'doc'."
4.32 +
4.33 + self.node = node
4.34 + self.doc = doc
4.35 +
4.36 +# vim: tabstop=4 expandtab shiftwidth=4
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/moinformat/macros/manifest.py Tue Jul 24 23:37:44 2018 +0200
5.3 @@ -0,0 +1,47 @@
5.4 +#!/usr/bin/env python
5.5 +
5.6 +"""
5.7 +Moin macro implementation manifest.
5.8 +
5.9 +Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk>
5.10 +
5.11 +This program is free software; you can redistribute it and/or modify it under
5.12 +the terms of the GNU General Public License as published by the Free Software
5.13 +Foundation; either version 3 of the License, or (at your option) any later
5.14 +version.
5.15 +
5.16 +This program is distributed in the hope that it will be useful, but WITHOUT
5.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
5.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
5.19 +details.
5.20 +
5.21 +You should have received a copy of the GNU General Public License along with
5.22 +this program. If not, see <http://www.gnu.org/licenses/>.
5.23 +"""
5.24 +
5.25 +from moinformat.imports import get_extensions
5.26 +from os.path import split
5.27 +
5.28 +reserved = ["__init__", "common", "manifest"]
5.29 +
5.30 +# Obtain details of this module's package.
5.31 +
5.32 +dirname = split(__file__)[0]
5.33 +package = __name__.rsplit(".", 1)[0]
5.34 +
5.35 +# Define an attribute mapping names to modules.
5.36 +
5.37 +modules = {}
5.38 +get_extensions(dirname, package, modules, reserved)
5.39 +
5.40 +# Obtain all macros.
5.41 +
5.42 +macros = {}
5.43 +
5.44 +# Use names declared in each handler to register the handlers:
5.45 +# macro.name -> macro
5.46 +
5.47 +for module in modules.values():
5.48 + macros[module.macro.name] = module.macro
5.49 +
5.50 +# vim: tabstop=4 expandtab shiftwidth=4
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
6.2 +++ b/moinformat/macros/toc.py Tue Jul 24 23:37:44 2018 +0200
6.3 @@ -0,0 +1,153 @@
6.4 +#!/usr/bin/env python
6.5 +
6.6 +"""
6.7 +Table of contents macro.
6.8 +
6.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
6.10 +
6.11 +This program is free software; you can redistribute it and/or modify it under
6.12 +the terms of the GNU General Public License as published by the Free Software
6.13 +Foundation; either version 3 of the License, or (at your option) any later
6.14 +version.
6.15 +
6.16 +This program is distributed in the hope that it will be useful, but WITHOUT
6.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
6.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
6.19 +details.
6.20 +
6.21 +You should have received a copy of the GNU General Public License along with
6.22 +this program. If not, see <http://www.gnu.org/licenses/>.
6.23 +"""
6.24 +
6.25 +from moinformat.macros.common import Macro
6.26 +from moinformat.tree.moin import Container, Heading, List, ListItem, Text
6.27 +
6.28 +class TableOfContents(Macro):
6.29 +
6.30 + "A table of contents macro."
6.31 +
6.32 + name = "TableOfContents"
6.33 +
6.34 + def evaluate(self):
6.35 +
6.36 + "Evaluate the macro, producing a table of contents."
6.37 +
6.38 + arglist = []
6.39 + _defaults = [None] * 2
6.40 +
6.41 + for arg, default in map(None, self.node.args, _defaults):
6.42 + if arg is not None:
6.43 + try:
6.44 + arg = max(1, int(arg.strip()))
6.45 + except ValueError:
6.46 + arg = None
6.47 + arglist.append(arg)
6.48 +
6.49 + self.make_table(arglist[0], arglist[1])
6.50 +
6.51 + def make_table(self, min_level=None, max_level=None):
6.52 +
6.53 + """
6.54 + Make a table of contents with the given 'min_level' and 'max_level' of
6.55 + headings.
6.56 + """
6.57 +
6.58 + headings = []
6.59 + self.find_headings(self.doc, headings)
6.60 +
6.61 + if not headings:
6.62 + return
6.63 +
6.64 + # Common list features.
6.65 +
6.66 + marker = "1."
6.67 + space = " "
6.68 + num = "1"
6.69 + nl = [Text("\n")]
6.70 +
6.71 + # Start with no lists, no current item.
6.72 +
6.73 + lists = []
6.74 + item = None
6.75 + level = 0
6.76 +
6.77 + for heading in headings:
6.78 + new_level = heading.level
6.79 +
6.80 + # Create new lists if the level increases.
6.81 +
6.82 + if new_level > level:
6.83 + while level < new_level:
6.84 + level += 1
6.85 +
6.86 + if not (min_level <= level <= max_level):
6.87 + continue
6.88 +
6.89 + # Determine whether the heading should be generated at this
6.90 + # level.
6.91 +
6.92 + nodes = level == new_level and heading.nodes[:] + nl or []
6.93 + indent = level - 1
6.94 +
6.95 + # Make a list and add an item to it.
6.96 +
6.97 + new_items = []
6.98 + new_list = List(new_items, indent, marker, num)
6.99 + new_item = ListItem(nodes, indent, marker, space, None)
6.100 + new_items.append(new_item)
6.101 +
6.102 + # Add the list to the current item, if any.
6.103 +
6.104 + if item:
6.105 + item.nodes.append(new_list)
6.106 +
6.107 + # Record the new list.
6.108 +
6.109 + lists.append(new_list)
6.110 +
6.111 + # Reference the new list's items and current item.
6.112 +
6.113 + items = new_items
6.114 + item = new_item
6.115 +
6.116 + else:
6.117 + # Retrieve an existing list if the level decreases.
6.118 +
6.119 + if new_level < level:
6.120 + while level > new_level:
6.121 + if min_level <= level <= max_level:
6.122 + lists.pop()
6.123 + level -= 1
6.124 +
6.125 + # Obtain the existing list and the current item.
6.126 +
6.127 + items = lists[-1].nodes
6.128 + item = items[-1]
6.129 +
6.130 + # Add the heading as an item.
6.131 +
6.132 + if min_level <= level <= max_level:
6.133 + indent = level - 1
6.134 + nodes = heading.nodes[:] + nl
6.135 +
6.136 + item = ListItem(nodes, indent, marker, space, None)
6.137 + items.append(item)
6.138 +
6.139 + # Replace the macro node's children with the top-level list.
6.140 +
6.141 + self.node.nodes = [lists[0]]
6.142 +
6.143 + def find_headings(self, node, headings):
6.144 +
6.145 + "Find headings under 'node', adding them to the 'headings' list."
6.146 +
6.147 + if node.nodes:
6.148 + for n in node.nodes:
6.149 + if isinstance(n, Heading):
6.150 + headings.append(n)
6.151 + elif isinstance(n, Container):
6.152 + self.find_headings(n, headings)
6.153 +
6.154 +macro = TableOfContents
6.155 +
6.156 +# vim: tabstop=4 expandtab shiftwidth=4
7.1 --- a/moinformat/parsers/__init__.py Tue Jul 24 15:53:44 2018 +0200
7.2 +++ b/moinformat/parsers/__init__.py Tue Jul 24 23:37:44 2018 +0200
7.3 @@ -3,7 +3,7 @@
7.4 """
7.5 Moin wiki parsers.
7.6
7.7 -Copyright (C) 2017 Paul Boddie <paul@boddie.org.uk>
7.8 +Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk>
7.9
7.10 This program is free software; you can redistribute it and/or modify it under
7.11 the terms of the GNU General Public License as published by the Free Software
7.12 @@ -20,10 +20,15 @@
7.13 """
7.14
7.15 from moinformat.parsers.manifest import parsers
7.16 +from moinformat.parsers.moin import MoinParser
7.17
7.18 # Top-level functions.
7.19
7.20 -def parse(s, formats=None):
7.21 - return parsers["moin"](formats).parse(s)
7.22 +def parse(s, parser=None):
7.23 +
7.24 + "Parse 's' with 'parser' or the Moin format parser if omitted."
7.25 +
7.26 + parser = parser or MoinParser(parsers)
7.27 + return parser.parse(s)
7.28
7.29 # vim: tabstop=4 expandtab shiftwidth=4
8.1 --- a/moinformat/parsers/common.py Tue Jul 24 15:53:44 2018 +0200
8.2 +++ b/moinformat/parsers/common.py Tue Jul 24 23:37:44 2018 +0200
8.3 @@ -253,14 +253,16 @@
8.4
8.5 region_pattern_names = None
8.6
8.7 - def __init__(self, formats=None):
8.8 + def __init__(self, formats=None, root=None):
8.9
8.10 """
8.11 Initialise the parser with any given 'formats' mapping from region type
8.12 - names to parser objects.
8.13 + names to parser objects. An optional 'root' indicates the document-level
8.14 + parser.
8.15 """
8.16
8.17 self.formats = formats
8.18 + self.root = root
8.19
8.20 def get_parser(self, format_type):
8.21
8.22 @@ -273,7 +275,7 @@
8.23
8.24 cls = self.formats.get(format_type)
8.25 if cls:
8.26 - return cls(self.formats)
8.27 + return cls(self.formats, self.root or self)
8.28 else:
8.29 return None
8.30
9.1 --- a/moinformat/parsers/moin.py Tue Jul 24 15:53:44 2018 +0200
9.2 +++ b/moinformat/parsers/moin.py Tue Jul 24 23:37:44 2018 +0200
9.3 @@ -19,6 +19,7 @@
9.4 this program. If not, see <http://www.gnu.org/licenses/>.
9.5 """
9.6
9.7 +from moinformat.macros import get_macro
9.8 from moinformat.parsers.common import ParserBase, get_patterns, \
9.9 excl, expect, group, optional, recur, \
9.10 repeat
9.11 @@ -36,11 +37,12 @@
9.12
9.13 "A wiki region parser."
9.14
9.15 - def __init__(self, formats=None):
9.16 + def __init__(self, formats=None, root=None):
9.17
9.18 """
9.19 Initialise the parser with any given 'formats' mapping from region type
9.20 - names to parser objects.
9.21 + names to parser objects. An optional 'root' indicates the document-level
9.22 + parser.
9.23 """
9.24
9.25 # Introduce this class as the default parser for the wiki format.
9.26 @@ -49,7 +51,11 @@
9.27 if formats:
9.28 default_formats.update(formats)
9.29
9.30 - ParserBase.__init__(self, default_formats)
9.31 + ParserBase.__init__(self, default_formats, root)
9.32 +
9.33 + # Record macro occurrences for later evaluation.
9.34 +
9.35 + self.macros = []
9.36
9.37 # Principal parser methods.
9.38
9.39 @@ -81,6 +87,27 @@
9.40
9.41
9.42
9.43 + # Macro evaluation.
9.44 +
9.45 + def evaluate_macros(self):
9.46 +
9.47 + "Evaluate the macro nodes in the document."
9.48 +
9.49 + for node in self.macros:
9.50 +
9.51 + # Obtain a class for the named macro.
9.52 +
9.53 + macro_cls = get_macro(node.name)
9.54 + if not macro_cls:
9.55 + continue
9.56 +
9.57 + # Instantiate the class and evaluate the macro.
9.58 +
9.59 + macro = macro_cls(node, self.region)
9.60 + macro.evaluate()
9.61 +
9.62 +
9.63 +
9.64 # Parser methods supporting different page features.
9.65
9.66 def parse_attrname(self, attrs):
9.67 @@ -464,6 +491,10 @@
9.68 macro = Macro(name, arglist)
9.69 region.append_inline(macro)
9.70
9.71 + # Record the macro for later processing.
9.72 +
9.73 + self.root.macros.append(macro)
9.74 +
9.75
9.76
9.77 # Table attribute handlers.
10.1 --- a/moinformat/serialisers/html/moin.py Tue Jul 24 15:53:44 2018 +0200
10.2 +++ b/moinformat/serialisers/html/moin.py Tue Jul 24 23:37:44 2018 +0200
10.3 @@ -141,6 +141,29 @@
10.4 def end_listitem(self, indent, marker, space, num):
10.5 self.out("</li>")
10.6
10.7 + def start_macro(self, name, args, nodes):
10.8 + self.out("<span class='macro'>")
10.9 +
10.10 + # Fallback case for when macros are not replaced.
10.11 +
10.12 + if not nodes:
10.13 + self.out(escape_text("<<"))
10.14 + self.out("<span class='name'>%s</span>" % escape_text(name))
10.15 + if args:
10.16 + self.out("(")
10.17 + first = True
10.18 + for arg in args:
10.19 + if not first:
10.20 + self.out(",")
10.21 + self.out("<span class='arg'>%s</span>" % escape_text(arg))
10.22 + first = False
10.23 + if args:
10.24 + self.out(")")
10.25 + self.out(escape_text(">>"))
10.26 +
10.27 + def end_macro(self):
10.28 + self.out("</span>")
10.29 +
10.30 def start_monospace(self):
10.31 self.out("<tt>")
10.32
10.33 @@ -213,32 +236,6 @@
10.34 def break_(self):
10.35 pass
10.36
10.37 - def macro(self, name, args):
10.38 -
10.39 - # NOTE: Special case.
10.40 -
10.41 - if name == "BR":
10.42 - self.out("<br />")
10.43 - return
10.44 -
10.45 - # Fallback case.
10.46 -
10.47 - self.out("<span class='macro'>")
10.48 - self.out(escape_text("<<"))
10.49 - self.out("<span class='name'>%s</span>" % escape_text(name))
10.50 - if args:
10.51 - self.out("(")
10.52 - first = True
10.53 - for arg in args:
10.54 - if not first:
10.55 - self.out(",")
10.56 - self.out("<span class='arg'>%s</span>" % escape_text(arg))
10.57 - first = False
10.58 - if args:
10.59 - self.out(")")
10.60 - self.out(escape_text(">>"))
10.61 - self.out("</span>")
10.62 -
10.63 def rule(self, length):
10.64 self.out("<hr style='height: %dpt' />" % min(length, 10))
10.65
11.1 --- a/moinformat/serialisers/moin/moin.py Tue Jul 24 15:53:44 2018 +0200
11.2 +++ b/moinformat/serialisers/moin/moin.py Tue Jul 24 23:37:44 2018 +0200
11.3 @@ -102,6 +102,16 @@
11.4 def end_listitem(self, indent, marker, space, num):
11.5 pass
11.6
11.7 + def start_macro(self, name, args, nodes):
11.8 +
11.9 + # Fallback case for when macros are not replaced.
11.10 +
11.11 + if not nodes:
11.12 + self.out("<<%s%s>>" % (name, args and "(%s)" % ",".join(args) or ""))
11.13 +
11.14 + def end_macro(self):
11.15 + pass
11.16 +
11.17 def start_monospace(self):
11.18 self.out("`")
11.19
11.20 @@ -177,9 +187,6 @@
11.21 def continuation(self, text):
11.22 self.out(text)
11.23
11.24 - def macro(self, name, args):
11.25 - self.out("<<%s%s>>" % (name, args and "(%s)" % ",".join(args) or ""))
11.26 -
11.27 def rule(self, length):
11.28 self.out("-" * length)
11.29
12.1 --- a/moinformat/tree/moin.py Tue Jul 24 15:53:44 2018 +0200
12.2 +++ b/moinformat/tree/moin.py Tue Jul 24 23:37:44 2018 +0200
12.3 @@ -453,6 +453,28 @@
12.4 out.end_linktext()
12.5 out.end_link()
12.6
12.7 +class Macro(Container):
12.8 +
12.9 + "Macro details."
12.10 +
12.11 + def __init__(self, name, args, nodes=None):
12.12 + Container.__init__(self, nodes or [])
12.13 + self.name = name
12.14 + self.args = args
12.15 +
12.16 + def __repr__(self):
12.17 + return "Macro(%r, %r, %r)" % (self.name, self.args, self.nodes)
12.18 +
12.19 + def prettyprint(self, indent=""):
12.20 + l = ["%sMacro: name=%r args=%r" % (indent, self.name, self.args)]
12.21 + return self._prettyprint(l, indent)
12.22 +
12.23 + def to_string(self, out):
12.24 + out.start_macro(self.name, self.args, self.nodes)
12.25 + if self.nodes:
12.26 + self._to_string(out)
12.27 + out.end_macro()
12.28 +
12.29 class Monospace(Inline):
12.30
12.31 "Monospaced text."
12.32 @@ -531,23 +553,6 @@
12.33 def to_string(self, out):
12.34 out.break_()
12.35
12.36 -class Macro(Node):
12.37 -
12.38 - "Macro details."
12.39 -
12.40 - def __init__(self, name, args):
12.41 - self.name = name
12.42 - self.args = args
12.43 -
12.44 - def __repr__(self):
12.45 - return "Macro(%r, %r)" % (self.name, self.args)
12.46 -
12.47 - def prettyprint(self, indent=""):
12.48 - return "%sMacro: name=%r args=%r" % (indent, self.name, self.args)
12.49 -
12.50 - def to_string(self, out):
12.51 - out.macro(self.name, self.args)
12.52 -
12.53 class Rule(Node):
12.54
12.55 "A horizontal rule."
13.1 --- a/tests/test_parser.py Tue Jul 24 15:53:44 2018 +0200
13.2 +++ b/tests/test_parser.py Tue Jul 24 23:37:44 2018 +0200
13.3 @@ -12,7 +12,7 @@
13.4 if split(parent)[1] == "MoinLight":
13.5 sys.path.append(parent)
13.6
13.7 -from moinformat import all_parsers, get_serialiser, parse, serialise
13.8 +from moinformat import get_serialiser, parse, serialise
13.9 from moinformat.tree.moin import Container
13.10 from glob import glob
13.11
13.12 @@ -185,7 +185,7 @@
13.13 tree_filename = "%s.tree" % filename.rsplit(".", 1)[0]
13.14
13.15 s = readfile(filename)
13.16 - d = parse(s, all_parsers)
13.17 + d = parse(s)
13.18
13.19 if exists(tree_filename):
13.20 ts = readfile(tree_filename)