# HG changeset patch # User Paul Boddie # Date 1532905611 -7200 # Node ID cd7bd631b2e18ff4d7f659a4f5d2a580082f75c2 # Parent 437e2de72754bb46f6935e55eb9935e12c1ca9a3# Parent 375cfd1f31618c42d4394e19446491a9cf24940c Merged changes from the default branch. diff -r 437e2de72754 -r cd7bd631b2e1 convert.py --- a/convert.py Thu Jul 26 20:10:38 2018 +0200 +++ b/convert.py Mon Jul 30 01:06:51 2018 +0200 @@ -1,6 +1,7 @@ #!/usr/bin/env python -from moinformat import make_linker, make_parser, make_serialiser, parse, serialise +from moinformat import make_linker, make_output, make_parser, make_serialiser, \ + parse, serialise from os.path import split import sys @@ -11,6 +12,8 @@ l = filenames = [] formats = [] pagenames = [] + mappings = [] + outputs = [] tree = False macros = False @@ -32,6 +35,18 @@ l = formats continue + # Switch to collecting mappings. + + elif arg == "--mapping": + l = mappings + continue + + # Switch to collecting output locations. + + elif arg == "--output": + l = outputs + continue + # Switch to collecting page names. elif arg == "--pagename": @@ -43,6 +58,11 @@ else: l.append(arg) + # Collect multiple mappings. + + if l is mappings: + continue + # Collect filenames normally. l = filenames @@ -54,6 +74,24 @@ filename = filenames[0] pagename = pagenames and pagenames[0] or split(filename)[-1] + # Obtain an output context from any specified output details. + + output = outputs and make_output(outputs[0]) or None + + # Derive a proper mapping from the given list of values. + + mapping = {} + key = None + + for arg in mappings: + if key is None: + key = arg + else: + mapping[key] = arg + key = None + + # Open the file, parse the content, serialise the document. + f = open(filename) try: p = make_parser() @@ -65,8 +103,8 @@ if tree: print d.prettyprint() else: - l = make_linker(format, pagename) - s = make_serialiser(format, l) + l = make_linker(format, pagename, mapping) + s = make_serialiser(format, output, l) print serialise(d, s) finally: f.close() diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/__init__.py --- a/moinformat/__init__.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -20,37 +20,8 @@ """ from moinformat.links import make_linker -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 class for the format having the given 'name'." - - return all_serialisers["%s.moin" % name] - -def make_serialiser(name, linker=None): - - """ - Return a serialiser instance for the format having the given 'name'. - - The optional 'linker' is used to control which linking scheme is used with - the serialiser, with the default having the same name as the serialiser. - """ - - linker = linker or make_linker(name, "") - return get_serialiser(name)(formats=all_serialisers, linker=linker) +from moinformat.output import make_output +from moinformat.parsers import get_parser, make_parser, parse +from moinformat.serialisers import get_serialiser, make_serialiser, serialise # vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/links/__init__.py --- a/moinformat/links/__init__.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/links/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -32,17 +32,17 @@ return linkers.get(name) -def make_linker(name, pagename): +def make_linker(name, pagename, mapping=None): """ Return a linking scheme handler with the given 'name' and using the given - 'pagename'. + 'pagename' and interwiki 'mapping'. """ linker_cls = get_linker(name) if not linker_cls: return None - return linker_cls(pagename) + return linker_cls(pagename, mapping) # vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/output/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/output/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +""" +Output contexts. + +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.output.directory import DirectoryOutput + +def make_output(dirname): + + "Return a directory output context employing 'dirname'." + + return DirectoryOutput(dirname) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/output/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/output/common.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +""" +Output context common 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 Output: + + "A common output context abstraction." + + def __init__(self): + + "Initialise the output context." + + self.output = [] + self.out = self.output.append + + def to_string(self): + + "Return the output as a string." + + return "".join(self.output) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/output/directory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/output/directory.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +""" +Directory output context. + +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.output.common import Output +from os.path import abspath, commonprefix, join + +def inside(filename, dirname): + + "Return whether 'filename' is inside 'dirname'." + + # Get the directory with trailing path separator. + + dirname = join(dirname, "") + return commonprefix((filename, dirname)) == dirname + + + +class DirectoryOutput(Output): + + "A directory output context." + + def __init__(self, filename): + + "Initialise the context with the given 'filename'." + + Output.__init__(self) + self.filename = abspath(filename) + + def get_filename(self, filename): + + "Return a file with the given 'filename' within the directory." + + # Get the absolute path for the combination of directory and filename. + + pathname = abspath(join(self.filename, filename)) + + if inside(pathname, self.filename): + return pathname + else: + raise ValueError, filename + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/parsers/__init__.py --- a/moinformat/parsers/__init__.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/parsers/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -24,6 +24,18 @@ # Top-level functions. +def get_parser(name="moin"): + + "Return the parser class supporting the format with the given 'name'." + + return parsers[name] + +def make_parser(name="moin"): + + "Return a parser instance for the format with the given 'name'." + + return get_parser(name)(parsers) + def parse(s, parser=None): "Parse 's' with 'parser' or the Moin format parser if omitted." diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/parsers/common.py --- a/moinformat/parsers/common.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/parsers/common.py Mon Jul 30 01:06:51 2018 +0200 @@ -486,4 +486,19 @@ self.add_node(region, Block([])) + # Common handler methods. + + def parse_region_end(self, node): + + "Handle the end of a region occurring within 'node'." + + level = self.match_group("level") + feature = self.match_group("feature") + self.region.extra = self.match_group("extra") + + if self.region.have_end(level): + raise StopIteration + else: + node.append_inline(Text(feature)) + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/parsers/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/parsers/graphviz.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +""" +Graphviz region metadata parser. This only identifies metadata, with the actual +graph data being interpreted by Graphviz itself. + +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.parsers.common import ParserBase, get_patterns, group, optional +from moinformat.parsers.moin import MoinParser +from moinformat.tree.graphviz import Directive +from moinformat.tree.moin import Text + +join = "".join + +# Parser functionality. + +class GraphvizParser(ParserBase): + + "A parser for Graphviz content, identifying format directives." + + # Parser handler methods. + + def parse_directive(self, region): + + "Handle format directives." + + key = self.match_group("key") + value = self.match_group("value") + self.add_node(region, Directive(key, value)) + self.new_block(region) + + + + # Regular expressions. + + syntax = { + # At start of line: + + "directive" : join(("^//", # // + group("key", ".*?"), # text-excl-eq-nl + optional(join(("=", # eq (optional) + group("value", ".*?")))), # text-excl-nl (optional) + "\n")), # nl + + "regionend" : MoinParser.syntax["regionend"], + } + + patterns = get_patterns(syntax) + + + + # Pattern details. + + region_pattern_names = ["directive", "regionend"] + + + + # Pattern handlers. + + parse_region_end = ParserBase.parse_region_end + + handlers = { + "directive" : parse_directive, + "regionend" : parse_region_end, + } + +parser = GraphvizParser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/parsers/moin.py --- a/moinformat/parsers/moin.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/parsers/moin.py Mon Jul 30 01:06:51 2018 +0200 @@ -330,25 +330,12 @@ if region.allow_blocks: self.new_block(region) - def parse_section_end(self, region): - - "Handle the end of a new section within 'region'." - - level = self.match_group("level") - feature = self.match_group("feature") - region.extra = self.match_group("extra") - - if region.have_end(level): - raise StopIteration - else: - region.append_inline(Text(feature)) - def parse_table_attrs(self, cell): "Handle the start of table attributes within 'cell'." attrs = TableAttrs([]) - self.parse_region_details(attrs, self.table_pattern_names) + self.parse_region_details(attrs, self.table_attr_pattern_names) # Test the validity of the attributes. @@ -400,7 +387,7 @@ while True: cell = TableCell([]) - self.parse_region_details(cell, self.table_region_pattern_names) + self.parse_region_details(cell, self.table_row_pattern_names) # Handle the end of the row. @@ -448,6 +435,13 @@ + def inline_patterns_for(self, name): + names = self.inline_pattern_names[:] + names[names.index(name)] = "%send" % name + return names + + + # Inline formatting handlers. def parse_inline(self, region, cls, pattern_name): @@ -697,7 +691,7 @@ # Patterns available within certain markup features. - table_pattern_names = [ + table_attr_pattern_names = [ "attrname", "colour", "colspan", "halign", "rowspan", "tableattrsend", "valign", "width" ] @@ -719,22 +713,21 @@ "regionend", "rule", ] - region_pattern_names = region_without_table_pattern_names + ["tablerow"] - - table_region_pattern_names = inline_pattern_names + [ + table_row_pattern_names = inline_pattern_names + [ "tableattrs", "tablecell", "tableend" ] - def inline_patterns_for(self, name): - names = self.inline_pattern_names[:] - names[names.index(name)] = "%send" % name - return names + # The region pattern names are specifically used by the common parser + # functionality. + + region_pattern_names = region_without_table_pattern_names + ["tablerow"] # Pattern handlers. end_region = ParserBase.end_region + parse_section_end = ParserBase.parse_region_end handlers = { None : end_region, diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/parsers/table.py --- a/moinformat/parsers/table.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/parsers/table.py Mon Jul 30 01:06:51 2018 +0200 @@ -37,12 +37,13 @@ def parse_region_content(self, items, region): - "Parse the data provided by 'items' to populate the given 'region'." + """ + Parse the data provided by 'items' to populate the given 'region'. For + table regions, normal region handling is wrapped by management of the + table structure. + """ self.set_region(items, region) - self.parse_table_region() - - def parse_table_region(self): # Start to populate table rows. @@ -52,7 +53,7 @@ self.append_node(self.region, table) while True: - self.parse_region_details(cell, self.table_region_pattern_names) + self.parse_region_details(cell, self.region_pattern_names) # Detect the end of the table. @@ -80,18 +81,7 @@ feature = self.match_group("feature") cell.append(Continuation(feature)) - def parse_table_end(self, cell): - "Handle the end of a region within 'cell'." - - level = self.match_group("level") - feature = self.match_group("feature") - self.region.extra = self.match_group("extra") - - if self.region.have_end(level): - raise StopIteration - else: - cell.append_inline(Text(feature)) # Regular expressions. @@ -122,20 +112,23 @@ # Pattern details. - table_region_pattern_names = [ - "columnsep", "continuation", "rowsep", + region_pattern_names = [ + "columnsep", "continuation", "rowsep", "tableattrs", ] + MoinParser.region_without_table_pattern_names # Pattern handlers. + end_region = MoinParser.end_region + parse_table_end = MoinParser.parse_region_end + handlers = {} handlers.update(MoinParser.handlers) handlers.update({ - "columnsep" : MoinParser.end_region, + "columnsep" : end_region, "continuation" : parse_continuation, - "rowsep" : MoinParser.end_region, + "rowsep" : end_region, "regionend" : parse_table_end, }) diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/__init__.py --- a/moinformat/serialisers/__init__.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/serialisers/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -19,11 +19,36 @@ this program. If not, see . """ +from moinformat.links import make_linker +from moinformat.output.directory import DirectoryOutput from moinformat.serialisers.manifest import serialisers from moinformat.serialisers.moin.moin import MoinSerialiser +from os.path import curdir # Top-level functions. +def get_serialiser(name): + + "Return the main serialiser class for the format having the given 'name'." + + return serialisers["%s.moin" % name] + +def make_serialiser(name, output=None, linker=None): + + """ + Return a serialiser instance for the format having the given 'name'. + + The optional 'output' context is used to control where separate resources + are stored, with the default being the current directory. + + The optional 'linker' is used to control which linking scheme is used with + the serialiser, with the default having the same name as the serialiser. + """ + + output = output or DirectoryOutput(curdir) + linker = linker or make_linker(name, "") + return get_serialiser(name)(output, serialisers, linker) + def serialise(doc, serialiser=None): """ @@ -31,10 +56,11 @@ if omitted. """ - l = [] - serialiser = serialiser or MoinSerialiser(formats=serialisers) - serialiser.out = l.append + if not serialiser: + output = DirectoryOutput(curdir) + serialiser = MoinSerialiser(output, serialisers) + doc.to_string(serialiser) - return "".join(l) + return serialiser.get_output() # vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/common.py --- a/moinformat/serialisers/common.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/serialisers/common.py Mon Jul 30 01:06:51 2018 +0200 @@ -25,17 +25,24 @@ format = None # defined by subclasses - def __init__(self, out=None, formats=None, linker=None): + def __init__(self, output, formats=None, linker=None): """ - Initialise the serialiser with an 'out' callable, an optional 'formats' - mapping from names to serialiser classes, and an optional 'linker' - object for translating links. + Initialise the serialiser with an 'output' context, an optional + 'formats' mapping from names to serialiser classes, and an optional + 'linker' object for translating links. """ - self.out = out + self.output = output self.formats = formats self.linker = linker + + # Initialise a callable for use in serialisation. + + self.out = output.out + + # Initialisation of any other state. + self.init() def init(self): @@ -45,9 +52,40 @@ pass def __repr__(self): - return "%s(%r, %r, %r)" % (self.__class__.__name__, self.out, + return "%s(%r, %r, %r)" % (self.__class__.__name__, self.output, self.formats, self.linker) + def get_serialiser(self, format): + + """ + Return a serialiser for the given 'format'. Return self if no suitable + serialiser can be obtained. + """ + + cls = self.formats and self.formats.get(format) + if cls: + return self.instantiate(cls) + else: + return self + + def get_output(self): + + "Return the output as a string." + + return self.output.to_string() + + def instantiate(self, cls): + + """ + Instantiate 'cls' and return the result if 'cls' is a different class to + this instance. Otherwise, return this instance. + """ + + if cls is self.__class__: + return self + else: + return cls(self.output, self.formats, self.linker) + def escape_attr(s): "Escape XML document attribute." diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/html/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/serialisers/html/graphviz.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +""" +Graphviz serialiser, generating content for embedding in HTML documents. + +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.serialisers.common import Serialiser, escape_attr, escape_text +from moinformat.utils.graphviz import Graphviz, GraphvizError, IMAGE_FORMATS, \ + get_output_identifier + +# Utility functions. + +def select_keys(d, keys): + + "Select from 'd' the given 'keys'." + + if not d: + return [] + + out = {} + + for key in keys: + if d.has_key(key): + out[key] = d[key] + + return out + + + +# The serialiser class. + +class HTMLGraphvizSerialiser(Serialiser): + + "Serialisation of Graphviz regions." + + def init(self): + self.directives = {} + + def start_block(self): + pass + + def end_block(self): + pass + + def directive(self, key, value): + if not self.directives.has_key(key): + self.directives[key] = [] + self.directives[key].append(value) + + def text(self, text): + self.process_graph(text) + + + + # Special methods for graph production. + + def _tag(self, tagname, attrname, filename, attributes, closing): + l = ["%s='%s'" % (attrname, escape_attr(filename))] + for key, value in attributes.items(): + l.append("%s='%s'" % (key, value)) + self.out("<%s %s%s>" % (tagname, " ".join(l), closing and " /")) + + def image(self, filename, attributes): + self._tag("img", "src", filename, attributes, True) + + def object(self, filename, attributes): + self._tag("object", "data", filename, attributes, False) + self.out("") + + def raw(self, text): + self.out(text) + + + + # Graph output preparation. + + def process_graph(self, text): + + "Process the graph 'text' using the known directives." + + filter = self.directives.get("filter", ["dot"])[0] + format = self.directives.get("format", ["svg"])[0] + transforms = self.directives.get("transform", []) + + # Get an identifier and usable filename to store the output. + + identifier = get_output_identifier(text) + filename = self.output.get_filename(identifier) + + # Permit imagemaps only for image formats. + + if format in IMAGE_FORMATS: + cmapx = self.directives.has_key("cmapx") + + # Configure Graphviz and invoke it. + + graphviz = Graphviz(filter, text, identifier) + graphviz.call(format, transforms, filename) + + # Obtain any metadata. + + attributes = select_keys(graphviz.get_metadata(), ["width", "height"]) + + # For image output, create a file directly and reference it. + + if format in IMAGE_FORMATS: + + # Produce, embed and reference an imagemap if requested. + + if cmapx: + graphviz.call("cmapx") + mapid = graphviz.get_metadata().get("id") + + if mapid: + self.raw(graphviz.get_output()) + attributes["usemap"] = "#%s" % im_attributes["id"] + + self.image(filename, attributes) + + # For other output, create a file and embed the object. + + else: + self.object(filename, attributes) + +serialiser = HTMLGraphvizSerialiser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/html/moin.py --- a/moinformat/serialisers/html/moin.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/serialisers/html/moin.py Mon Jul 30 01:06:51 2018 +0200 @@ -41,8 +41,12 @@ return "span" def start_region(self, level, indent, type, extra): + + # Generate attributes, joining them when preparing the tag. + l = [] out = l.append + if level: out("level-%d" % level) @@ -216,8 +220,13 @@ def start_table_cell(self, attrs): self.out("") def end_table_cell(self): @@ -241,6 +250,12 @@ def rule(self, length): self.out("
" % min(length, 10)) + def table_attrs(self, nodes): + + # Skip the attributes in their original form. + + pass + def table_attr(self, name, value, concise, quote): self.out(" %s%s" % (escape_text(name), value is not None and "='%s'" % escape_attr(value) or "")) diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/moin/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/serialisers/moin/graphviz.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +""" +Moin Graphviz region serialiser. + +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.serialisers.common import Serialiser + +class MoinGraphvizSerialiser(Serialiser): + + "Serialisation of the page." + + def start_block(self): + pass + + def end_block(self): + pass + + def directive(self, key, value): + self.out("//%s%s\n" % (value and "%s=" % key or key, value or "")) + + def text(self, text): + self.out(text) + +serialiser = MoinGraphvizSerialiser + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/moin/moin.py --- a/moinformat/serialisers/moin/moin.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/serialisers/moin/moin.py Mon Jul 30 01:06:51 2018 +0200 @@ -162,8 +162,6 @@ def start_table_cell(self, attrs): self.out("||") - if attrs and not attrs.empty(): - attrs.to_string(self) def end_table_cell(self): pass @@ -190,6 +188,10 @@ def rule(self, length): self.out("-" * length) + def table_attrs(self, nodes): + for node in nodes: + node.to_string(self) + def table_attr(self, name, value, concise, quote): if concise: if name == "colour": self.out(value) diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/serialisers/moin/table.py --- a/moinformat/serialisers/moin/table.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/serialisers/moin/table.py Mon Jul 30 01:06:51 2018 +0200 @@ -38,9 +38,6 @@ else: self.first_cell = False - if attrs and not attrs.empty(): - attrs.to_string(self) - def start_table_row(self): self.first_cell = True if not self.first_row: diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/tree/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/tree/graphviz.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +""" +Graphviz document tree nodes. + +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.tree.moin import Node + +class Directive(Node): + + "Format directive for Graphviz output." + + def __init__(self, key=None, value=None): + self.key = key + self.value = value + + def __repr__(self): + return "Directive(%r, %r)" % (self.key, self.value) + + def prettyprint(self, indent=""): + return "%sDirective: key=%r value=%r" % (indent, self.key, self.value) + + def to_string(self, out): + out.directive(self.key, self.value) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/tree/moin.py --- a/moinformat/tree/moin.py Thu Jul 26 20:10:38 2018 +0200 +++ b/moinformat/tree/moin.py Mon Jul 30 01:06:51 2018 +0200 @@ -138,21 +138,18 @@ def to_string(self, out): out.start_region(self.level, self.indent, self.type, self.extra) - # Obtain a serialiser class for the region from the same format family. - - serialiser_name = "%s.%s" % (out.format, self.type) - serialiser_cls = out.formats and out.formats.get(serialiser_name) - + # Obtain a serialiser for the region from the same format family. # Retain the same serialiser if no appropriate serialiser could be # obtained. - region_out = serialiser_cls and serialiser_cls is not out and \ - serialiser_cls(out.out, out.formats, out.linker) or \ - out + serialiser_name = "%s.%s" % (out.format, self.type) + serialiser = out.get_serialiser(serialiser_name) # Serialise the region. - self._to_string(region_out) + self._to_string(serialiser) + + # End the region with the previous serialiser. out.end_region(self.level, self.indent, self.type, self.extra) @@ -346,7 +343,7 @@ def to_string(self, out): out.start_table_attrs() - self._to_string(out) + out.table_attrs(self.nodes) out.end_table_attrs() class Table(Container): @@ -382,9 +379,7 @@ def to_string(self, out): out.start_table_cell(self.attrs) - for node in self.nodes: - if node is not self.attrs: - node.to_string(out) + self._to_string(out) out.end_table_cell() class TableRow(Container): diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/utils/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/utils/__init__.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +""" +Utilities. + +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 . +""" + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 moinformat/utils/graphviz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moinformat/utils/graphviz.py Mon Jul 30 01:06:51 2018 +0200 @@ -0,0 +1,267 @@ +#!/usr/bin/env python + +""" +Graphviz utilities. + +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 os.path import exists, join +from StringIO import StringIO +from subprocess import Popen, PIPE +import gzip +import sha +import xml.sax + +# Configurable paths and locations. + +DIAGRAM_TOOLS_PATH = "/home/paulb/Software/Graphical/diagram-tools" +GRAPHVIZ_PATH = "/usr/bin" +XSLT_PROCESSOR = "/usr/bin/xsltproc" + +# Graphviz "filter" programs performing layout. + +FILTERS = ['circo', 'dot', 'fdp', 'neato', 'twopi'] + +# Supported output formats. + +IMAGE_FORMATS = ['png', 'gif'] +SVG_FORMATS = ['svg', 'svgz'] + +OUTPUT_FORMATS = IMAGE_FORMATS + SVG_FORMATS + \ + ['dia', 'fig', 'hpgl', 'imap', 'mif', 'pcl', 'ps'] + +# XSL transformations for SVG output. + +TRANSFORMS = { + "notugly" : join(DIAGRAM_TOOLS_PATH, "notugly.xsl"), + } + + + +# Utility functions. + +def encode(s, encoding): + + "Encode 's' using 'encoding' if Unicode." + + if isinstance(s, unicode): + return s.encode(encoding) + else: + return s + +class MetadataParser(xml.sax.handler.ContentHandler): + + "Parse metadata from the svg element." + + def __init__(self): + self.attrs = {} + + def startElement(self, name, attrs): + if name == self.tagname: + self.attrs = dict(attrs) + + def parse(self, f): + + "Parse content from the file object 'f' using reasonable defaults." + + try: + parser = xml.sax.make_parser() + parser.setContentHandler(self) + parser.setErrorHandler(xml.sax.handler.ErrorHandler()) + parser.setFeature(xml.sax.handler.feature_external_ges, 0) + parser.parse(f) + finally: + f.close() + + def get_metadata(self, data, tagname): + + "Process 'data', returning attributes from 'tagname'." + + self.tagname = tagname + + f = StringIO(data) + try: + self.parse(f) + finally: + f.close() + + return self.attrs + +def get_output_identifier(text): + + "Return an output identifier for the given 'text'." + + return sha.new(encode(text, 'utf-8')).hexdigest() + +def get_program(filter): + + "Return the program for the given 'filter'." + + if not filter in FILTERS: + return None + else: + return join(GRAPHVIZ_PATH, filter) + +def transform_output(process, format, transforms): + + "Transform the output from 'process' as 'format' using 'transforms'." + + # No transformation can occur if the processor is missing. + + if not exists(XSLT_PROCESSOR): + return process + + # Chain transformation processors, each accepting the output of the + # preceding one, with the first accepting the initial Graphviz output. + + for transform in transforms: + stylesheet = TRANSFORMS.get(transform) + + # Ignore unrecognised or missing stylesheets. + + if not stylesheet or not exists(stylesheet): + continue + + # Invoke the processor, indicating standard input as the source + # document. + # Example: /usr/bin/dot /usr/local/share/diagram-tools/notugly.xsl - + + process = Popen( + [XSLT_PROCESSOR, stylesheet, "-"], + shell=False, + stdin=process.stdout, + stdout=PIPE, + stderr=PIPE, + close_fds=True) + + return process + +def writefile(s, filename, compressed=False): + + "Write 's' to the file having 'filename'." + + if compressed: + f = gzip.open(filename, "w") + else: + f = open(filename, "w") + + try: + f.write(s) + finally: + f.close() + + + +# Classes for interacting with Graphviz. + +class GraphvizError(Exception): + + "An error produced when using Graphviz." + + def __init__(self, errors): + self.errors = errors + +class Graphviz: + + "A Graphviz configuration for single or repeated invocation." + + def __init__(self, filter, text, identifier): + + """ + Employ the given 'filter' to produce a graph from the given 'text'. The + output 'identifier' for the text is used to provide a filename, if + required. + """ + + self.filter = filter + self.text = text + self.identifier = identifier + + def call(self, format, transforms=None, filename=None): + + """ + Invoke Graphviz to produce output in the given 'format'. Any + 'transforms' are used to transform the output, if appropriate. Any + given 'filename' is used to write to a file. + """ + + program = get_program(self.filter) + + # Generate uncompressed SVG for later compression. + + graphviz_format = format == "svgz" and "svg" or format + + # Indicate a filename for direct output for non-SVG formats. + + svg = format in SVG_FORMATS + options = filename and not svg and ["-o", filename] or [] + + # Invoke the layout program, with the text to be provided on its + # standard input. + # Example: /usr/bin/dot -Tsvg -o filename + + start = end = Popen( + [program, '-T%s' % graphviz_format] + options, + shell=False, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE) + + # Chain the invocation to transformations, if appropriate. + + if svg and transforms: + end = transform_output(start, format, transforms) + + # Send the graph to the filter. + + start.stdin.write(encode(self.text, 'utf-8')) + + if end is not start: + start.stdin.close() + + # Obtain the eventual output. + + (self.output, errors) = end.communicate() + + # Obtain any metadata. + + if svg: + parser = MetadataParser() + self.metadata = parser.get_metadata(self.output, "svg") + elif format == "cmapx": + parser = MetadataParser() + self.metadata = parser.get_metadata(self.output, "map") + else: + self.metadata = {} + + # Test for errors. + + if end.wait() != 0: + raise GraphvizError, errors + + # Write the file separately, if requested. + + if svg and filename: + writefile(self.get_output(), filename, format == "svgz") + + def get_metadata(self): + return self.metadata + + def get_output(self): + return self.output + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 437e2de72754 -r cd7bd631b2e1 tests/test_links.tree --- a/tests/test_links.tree Thu Jul 26 20:10:38 2018 +0200 +++ b/tests/test_links.tree Mon Jul 30 01:06:51 2018 +0200 @@ -22,3 +22,6 @@ Link Text Text + Link + Text + Text diff -r 437e2de72754 -r cd7bd631b2e1 tests/test_links.txt --- a/tests/test_links.txt Thu Jul 26 20:10:38 2018 +0200 +++ b/tests/test_links.txt Mon Jul 30 01:06:51 2018 +0200 @@ -1,3 +1,4 @@ Links: [[TopLevel|top-level]], [[/SubPage|sub-page]], [[/Sub/SubPage|sub-sub-page]], [[../Sibling|sibling]], [[../../ParentSibling|sibling of parent]], -[[http://www.python.org/|URL]], [[attachment:image.png|attachment]]. +[[http://www.python.org/|URL]], [[attachment:image.png|attachment]], +[[MoinMoin:RecentChanges|interwiki]]. diff -r 437e2de72754 -r cd7bd631b2e1 tests/test_table_parser.tree --- a/tests/test_table_parser.tree Thu Jul 26 20:10:38 2018 +0200 +++ b/tests/test_table_parser.tree Mon Jul 30 01:06:51 2018 +0200 @@ -19,3 +19,27 @@ Break Block Text + Break + Region + Table + TableRow + TableCell + TableAttrs + TableAttr + Text + TableCell + Text + TableAttrs + TableAttr + Text + TableRow + TableCell + Text + TableAttrs + TableAttr + Text + TableCell + TableAttrs + TableAttr + Text + Block diff -r 437e2de72754 -r cd7bd631b2e1 tests/test_table_parser.txt --- a/tests/test_table_parser.txt Thu Jul 26 20:10:38 2018 +0200 +++ b/tests/test_table_parser.txt Mon Jul 30 01:06:51 2018 +0200 @@ -7,3 +7,14 @@ }}} Wiki format again + +{{{#!table + +Top +|| + +Bottom +== + Bottom +|| Top +}}}