# HG changeset patch # User Paul Boddie # Date 1532468173 -7200 # Node ID 8f0697b0a53dea3c3589c9f5b2afd89de790afcb # Parent 36d2a24d166bb32fe6114cf87e68e1abd9675455 Added initial support for macro evaluation, providing a table of contents macro. Made macro evaluation an option in the conversion script. diff -r 36d2a24d166b -r 8f0697b0a53d convert.py --- a/convert.py Tue Jul 24 18:45:32 2018 +0200 +++ b/convert.py Tue Jul 24 23:36:13 2018 +0200 @@ -1,16 +1,9 @@ #!/usr/bin/env python -from moinformat import 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()) + p = make_parser() + d = parse(f.read(), p) + + if macros: + p.evaluate_macros() + if tree: print d.prettyprint() else: diff -r 36d2a24d166b -r 8f0697b0a53d moinformat/macros/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/__init__.py Tue Jul 24 23:36:13 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 36d2a24d166b -r 8f0697b0a53d moinformat/macros/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/common.py Tue Jul 24 23:36:13 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 36d2a24d166b -r 8f0697b0a53d moinformat/macros/manifest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/manifest.py Tue Jul 24 23:36:13 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 36d2a24d166b -r 8f0697b0a53d moinformat/macros/toc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/macros/toc.py Tue Jul 24 23:36:13 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 36d2a24d166b -r 8f0697b0a53d moinformat/parsers/moin.py --- a/moinformat/parsers/moin.py Tue Jul 24 18:45:32 2018 +0200 +++ b/moinformat/parsers/moin.py Tue Jul 24 23:36:13 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 @@ -86,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): diff -r 36d2a24d166b -r 8f0697b0a53d moinformat/serialisers/html/moin.py --- a/moinformat/serialisers/html/moin.py Tue Jul 24 18:45:32 2018 +0200 +++ b/moinformat/serialisers/html/moin.py Tue Jul 24 23:36:13 2018 +0200 @@ -141,28 +141,28 @@ def end_listitem(self, indent, marker, space, num): self.out("") - def start_macro(self, name, args): + def start_macro(self, name, args, nodes): + self.out("") # Fallback case for when macros are not replaced. - 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("") + 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): - pass + self.out("") def start_monospace(self): self.out("") diff -r 36d2a24d166b -r 8f0697b0a53d moinformat/serialisers/moin/moin.py --- a/moinformat/serialisers/moin/moin.py Tue Jul 24 18:45:32 2018 +0200 +++ b/moinformat/serialisers/moin/moin.py Tue Jul 24 23:36:13 2018 +0200 @@ -102,8 +102,12 @@ def end_listitem(self, indent, marker, space, num): pass - def start_macro(self, name, args): - self.out("<<%s%s>>" % (name, args and "(%s)" % ",".join(args) or "")) + 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 diff -r 36d2a24d166b -r 8f0697b0a53d moinformat/tree/moin.py --- a/moinformat/tree/moin.py Tue Jul 24 18:45:32 2018 +0200 +++ b/moinformat/tree/moin.py Tue Jul 24 23:36:13 2018 +0200 @@ -470,7 +470,7 @@ return self._prettyprint(l, indent) def to_string(self, out): - out.start_macro(self.name, self.args) + out.start_macro(self.name, self.args, self.nodes) if self.nodes: self._to_string(out) out.end_macro()