# HG changeset patch # User Paul Boddie # Date 1532468264 -7200 # Node ID 519478d7f35fb06d6b15e5cdac50a4eeb0c7ce07 # Parent 645911c29199dd97e224ed3a4ce96a1cadba969b# Parent 8f0697b0a53dea3c3589c9f5b2afd89de790afcb Merged changes from the default branch. diff -r 645911c29199 -r 519478d7f35f convert.py --- a/convert.py Tue Jul 24 15:53:44 2018 +0200 +++ b/convert.py Tue Jul 24 23:37:44 2018 +0200 @@ -1,16 +1,9 @@ #!/usr/bin/env python -from moinformat import all_parsers, get_serialiser, parse, serialise +from moinformat import get_serialiser, make_parser, parse, serialise from os.path import split import sys -def test_option(args, name): - if name in args: - args.remove(name) - return True - else: - return False - def main(): dirname, progname = split(sys.argv[0]) args = sys.argv[1:] @@ -18,6 +11,7 @@ l = filenames = [] formats = [] tree = False + macros = False for arg in args: @@ -26,6 +20,11 @@ if arg == "--tree": tree = True + # Detect macro evaluation. + + elif arg == "--macros": + macros = True + # Switch to collecting formats elif arg == "--format": @@ -45,7 +44,12 @@ f = open(filename) try: - d = parse(f.read(), all_parsers) + p = make_parser() + d = parse(f.read(), p) + + if macros: + p.evaluate_macros() + if tree: print d.prettyprint() else: diff -r 645911c29199 -r 519478d7f35f moinformat/__init__.py --- a/moinformat/__init__.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/__init__.py Tue Jul 24 23:37:44 2018 +0200 @@ -22,9 +22,21 @@ from moinformat.parsers import parse, parsers as all_parsers from moinformat.serialisers import serialise, serialisers as all_serialisers +def get_parser(name="moin"): + + "Return the parser class supporting the format with the given 'name'." + + return all_parsers[name] + +def make_parser(name="moin"): + + "Return a parser instance for the format with the given 'name'." + + return get_parser(name)(all_parsers) + def get_serialiser(name): - "Return the main serialiser for the format having the given 'name'." + "Return the main serialiser class for the format having the given 'name'." return all_serialisers["%s.moin" % name] diff -r 645911c29199 -r 519478d7f35f moinformat/macros/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/__init__.py Tue Jul 24 23:37:44 2018 +0200 @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +""" +Moin macro implementations. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.macros.manifest import macros + +# Top-level functions. + +def get_macro(name): + + "Return the macro with the given 'name' or None if no macro is found." + + return macros.get(name) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 645911c29199 -r 519478d7f35f moinformat/macros/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/common.py Tue Jul 24 23:37:44 2018 +0200 @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +""" +Common macro functionality. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +class Macro: + + "Common macro functionality." + + def __init__(self, node, doc): + + "Initialise the macro with its tree 'node' and document root, 'doc'." + + self.node = node + self.doc = doc + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 645911c29199 -r 519478d7f35f moinformat/macros/manifest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/manifest.py Tue Jul 24 23:37:44 2018 +0200 @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +""" +Moin macro implementation manifest. + +Copyright (C) 2017, 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.imports import get_extensions +from os.path import split + +reserved = ["__init__", "common", "manifest"] + +# Obtain details of this module's package. + +dirname = split(__file__)[0] +package = __name__.rsplit(".", 1)[0] + +# Define an attribute mapping names to modules. + +modules = {} +get_extensions(dirname, package, modules, reserved) + +# Obtain all macros. + +macros = {} + +# Use names declared in each handler to register the handlers: +# macro.name -> macro + +for module in modules.values(): + macros[module.macro.name] = module.macro + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 645911c29199 -r 519478d7f35f moinformat/macros/toc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/toc.py Tue Jul 24 23:37:44 2018 +0200 @@ -0,0 +1,153 @@ +#!/usr/bin/env python + +""" +Table of contents macro. + +Copyright (C) 2018 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from moinformat.macros.common import Macro +from moinformat.tree.moin import Container, Heading, List, ListItem, Text + +class TableOfContents(Macro): + + "A table of contents macro." + + name = "TableOfContents" + + def evaluate(self): + + "Evaluate the macro, producing a table of contents." + + arglist = [] + _defaults = [None] * 2 + + for arg, default in map(None, self.node.args, _defaults): + if arg is not None: + try: + arg = max(1, int(arg.strip())) + except ValueError: + arg = None + arglist.append(arg) + + self.make_table(arglist[0], arglist[1]) + + def make_table(self, min_level=None, max_level=None): + + """ + Make a table of contents with the given 'min_level' and 'max_level' of + headings. + """ + + headings = [] + self.find_headings(self.doc, headings) + + if not headings: + return + + # Common list features. + + marker = "1." + space = " " + num = "1" + nl = [Text("\n")] + + # Start with no lists, no current item. + + lists = [] + item = None + level = 0 + + for heading in headings: + new_level = heading.level + + # Create new lists if the level increases. + + if new_level > level: + while level < new_level: + level += 1 + + if not (min_level <= level <= max_level): + continue + + # Determine whether the heading should be generated at this + # level. + + nodes = level == new_level and heading.nodes[:] + nl or [] + indent = level - 1 + + # Make a list and add an item to it. + + new_items = [] + new_list = List(new_items, indent, marker, num) + new_item = ListItem(nodes, indent, marker, space, None) + new_items.append(new_item) + + # Add the list to the current item, if any. + + if item: + item.nodes.append(new_list) + + # Record the new list. + + lists.append(new_list) + + # Reference the new list's items and current item. + + items = new_items + item = new_item + + else: + # Retrieve an existing list if the level decreases. + + if new_level < level: + while level > new_level: + if min_level <= level <= max_level: + lists.pop() + level -= 1 + + # Obtain the existing list and the current item. + + items = lists[-1].nodes + item = items[-1] + + # Add the heading as an item. + + if min_level <= level <= max_level: + indent = level - 1 + nodes = heading.nodes[:] + nl + + item = ListItem(nodes, indent, marker, space, None) + items.append(item) + + # Replace the macro node's children with the top-level list. + + self.node.nodes = [lists[0]] + + def find_headings(self, node, headings): + + "Find headings under 'node', adding them to the 'headings' list." + + if node.nodes: + for n in node.nodes: + if isinstance(n, Heading): + headings.append(n) + elif isinstance(n, Container): + self.find_headings(n, headings) + +macro = TableOfContents + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 645911c29199 -r 519478d7f35f moinformat/parsers/__init__.py --- a/moinformat/parsers/__init__.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/parsers/__init__.py Tue Jul 24 23:37:44 2018 +0200 @@ -3,7 +3,7 @@ """ Moin wiki parsers. -Copyright (C) 2017 Paul Boddie +Copyright (C) 2017, 2018 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,10 +20,15 @@ """ from moinformat.parsers.manifest import parsers +from moinformat.parsers.moin import MoinParser # Top-level functions. -def parse(s, formats=None): - return parsers["moin"](formats).parse(s) +def parse(s, parser=None): + + "Parse 's' with 'parser' or the Moin format parser if omitted." + + parser = parser or MoinParser(parsers) + return parser.parse(s) # vim: tabstop=4 expandtab shiftwidth=4 diff -r 645911c29199 -r 519478d7f35f moinformat/parsers/common.py --- a/moinformat/parsers/common.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/parsers/common.py Tue Jul 24 23:37:44 2018 +0200 @@ -253,14 +253,16 @@ region_pattern_names = None - def __init__(self, formats=None): + def __init__(self, formats=None, root=None): """ Initialise the parser with any given 'formats' mapping from region type - names to parser objects. + names to parser objects. An optional 'root' indicates the document-level + parser. """ self.formats = formats + self.root = root def get_parser(self, format_type): @@ -273,7 +275,7 @@ cls = self.formats.get(format_type) if cls: - return cls(self.formats) + return cls(self.formats, self.root or self) else: return None diff -r 645911c29199 -r 519478d7f35f moinformat/parsers/moin.py --- a/moinformat/parsers/moin.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/parsers/moin.py Tue Jul 24 23:37:44 2018 +0200 @@ -19,6 +19,7 @@ this program. If not, see . """ +from moinformat.macros import get_macro from moinformat.parsers.common import ParserBase, get_patterns, \ excl, expect, group, optional, recur, \ repeat @@ -36,11 +37,12 @@ "A wiki region parser." - def __init__(self, formats=None): + def __init__(self, formats=None, root=None): """ Initialise the parser with any given 'formats' mapping from region type - names to parser objects. + names to parser objects. An optional 'root' indicates the document-level + parser. """ # Introduce this class as the default parser for the wiki format. @@ -49,7 +51,11 @@ if formats: default_formats.update(formats) - ParserBase.__init__(self, default_formats) + ParserBase.__init__(self, default_formats, root) + + # Record macro occurrences for later evaluation. + + self.macros = [] # Principal parser methods. @@ -81,6 +87,27 @@ + # Macro evaluation. + + def evaluate_macros(self): + + "Evaluate the macro nodes in the document." + + for node in self.macros: + + # Obtain a class for the named macro. + + macro_cls = get_macro(node.name) + if not macro_cls: + continue + + # Instantiate the class and evaluate the macro. + + macro = macro_cls(node, self.region) + macro.evaluate() + + + # Parser methods supporting different page features. def parse_attrname(self, attrs): @@ -464,6 +491,10 @@ macro = Macro(name, arglist) region.append_inline(macro) + # Record the macro for later processing. + + self.root.macros.append(macro) + # Table attribute handlers. diff -r 645911c29199 -r 519478d7f35f moinformat/serialisers/html/moin.py --- a/moinformat/serialisers/html/moin.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/serialisers/html/moin.py Tue Jul 24 23:37:44 2018 +0200 @@ -141,6 +141,29 @@ def end_listitem(self, indent, marker, space, num): self.out("") + def start_macro(self, name, args, nodes): + self.out("") + + # Fallback case for when macros are not replaced. + + if not nodes: + self.out(escape_text("<<")) + self.out("%s" % escape_text(name)) + if args: + self.out("(") + first = True + for arg in args: + if not first: + self.out(",") + self.out("%s" % escape_text(arg)) + first = False + if args: + self.out(")") + self.out(escape_text(">>")) + + def end_macro(self): + self.out("") + def start_monospace(self): self.out("") @@ -213,32 +236,6 @@ def break_(self): pass - def macro(self, name, args): - - # NOTE: Special case. - - if name == "BR": - self.out("
") - return - - # Fallback case. - - self.out("") - self.out(escape_text("<<")) - self.out("%s" % escape_text(name)) - if args: - self.out("(") - first = True - for arg in args: - if not first: - self.out(",") - self.out("%s" % escape_text(arg)) - first = False - if args: - self.out(")") - self.out(escape_text(">>")) - self.out("") - def rule(self, length): self.out("
" % min(length, 10)) diff -r 645911c29199 -r 519478d7f35f moinformat/serialisers/moin/moin.py --- a/moinformat/serialisers/moin/moin.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/serialisers/moin/moin.py Tue Jul 24 23:37:44 2018 +0200 @@ -102,6 +102,16 @@ def end_listitem(self, indent, marker, space, num): pass + def start_macro(self, name, args, nodes): + + # Fallback case for when macros are not replaced. + + if not nodes: + self.out("<<%s%s>>" % (name, args and "(%s)" % ",".join(args) or "")) + + def end_macro(self): + pass + def start_monospace(self): self.out("`") @@ -177,9 +187,6 @@ def continuation(self, text): self.out(text) - def macro(self, name, args): - self.out("<<%s%s>>" % (name, args and "(%s)" % ",".join(args) or "")) - def rule(self, length): self.out("-" * length) diff -r 645911c29199 -r 519478d7f35f moinformat/tree/moin.py --- a/moinformat/tree/moin.py Tue Jul 24 15:53:44 2018 +0200 +++ b/moinformat/tree/moin.py Tue Jul 24 23:37:44 2018 +0200 @@ -453,6 +453,28 @@ out.end_linktext() out.end_link() +class Macro(Container): + + "Macro details." + + def __init__(self, name, args, nodes=None): + Container.__init__(self, nodes or []) + self.name = name + self.args = args + + def __repr__(self): + return "Macro(%r, %r, %r)" % (self.name, self.args, self.nodes) + + def prettyprint(self, indent=""): + l = ["%sMacro: name=%r args=%r" % (indent, self.name, self.args)] + return self._prettyprint(l, indent) + + def to_string(self, out): + out.start_macro(self.name, self.args, self.nodes) + if self.nodes: + self._to_string(out) + out.end_macro() + class Monospace(Inline): "Monospaced text." @@ -531,23 +553,6 @@ def to_string(self, out): out.break_() -class Macro(Node): - - "Macro details." - - def __init__(self, name, args): - self.name = name - self.args = args - - def __repr__(self): - return "Macro(%r, %r)" % (self.name, self.args) - - def prettyprint(self, indent=""): - return "%sMacro: name=%r args=%r" % (indent, self.name, self.args) - - def to_string(self, out): - out.macro(self.name, self.args) - class Rule(Node): "A horizontal rule." diff -r 645911c29199 -r 519478d7f35f tests/test_parser.py --- a/tests/test_parser.py Tue Jul 24 15:53:44 2018 +0200 +++ b/tests/test_parser.py Tue Jul 24 23:37:44 2018 +0200 @@ -12,7 +12,7 @@ if split(parent)[1] == "MoinLight": sys.path.append(parent) -from moinformat import all_parsers, get_serialiser, parse, serialise +from moinformat import get_serialiser, parse, serialise from moinformat.tree.moin import Container from glob import glob @@ -185,7 +185,7 @@ tree_filename = "%s.tree" % filename.rsplit(".", 1)[0] s = readfile(filename) - d = parse(s, all_parsers) + d = parse(s) if exists(tree_filename): ts = readfile(tree_filename)