1.1 --- a/convert.py Tue Aug 07 23:45:24 2018 +0200
1.2 +++ b/convert.py Mon Aug 13 22:57:16 2018 +0200
1.3 @@ -1,7 +1,7 @@
1.4 #!/usr/bin/env python
1.5
1.6 from moinformat import make_input, make_linker, make_output, make_parser, \
1.7 - make_serialiser, parse, serialise
1.8 + make_serialiser, make_theme, parse, serialise
1.9 from os.path import split
1.10 import sys
1.11
1.12 @@ -36,11 +36,13 @@
1.13 mappings = []
1.14 output_dirs = []
1.15 output_encodings = []
1.16 + theme_names = []
1.17 pagenames = []
1.18
1.19 # Flags.
1.20
1.21 all = False
1.22 + fragment = False
1.23 macros = False
1.24 tree = False
1.25
1.26 @@ -61,6 +63,11 @@
1.27 elif arg == "--all":
1.28 all = True
1.29
1.30 + # Detect fragment output (if serialising).
1.31 +
1.32 + elif arg == "--fragment":
1.33 + fragment = True
1.34 +
1.35 # Switch to collecting formats.
1.36
1.37 elif arg == "--format":
1.38 @@ -115,6 +122,12 @@
1.39 l = pagenames
1.40 continue
1.41
1.42 + # Switch to collecting theme names.
1.43 +
1.44 + elif arg == "--theme":
1.45 + l = theme_names
1.46 + continue
1.47 +
1.48 # Collect options and arguments.
1.49
1.50 else:
1.51 @@ -140,7 +153,7 @@
1.52 input_encoding = getvalue(input_encodings)
1.53 output_encoding = getvalue(output_encodings)
1.54
1.55 - # Obtain the input and output locations.
1.56 + # Obtain the input and output locations and contexts.
1.57
1.58 input_dir = getvalue(input_dirs)
1.59 output_dir = getvalue(output_dirs)
1.60 @@ -159,6 +172,12 @@
1.61 output = make_output(output_context, {"encoding" : output_encoding,
1.62 "filename" : output_dir})
1.63
1.64 + # Obtain a theme name.
1.65 +
1.66 + theme_name = not fragment and (getvalue(theme_names) or "default") or None
1.67 +
1.68 + theme = None
1.69 +
1.70 # Treat filenames as pagenames if an input directory is indicated and if no
1.71 # pagenames are explicitly specified.
1.72
1.73 @@ -223,6 +242,16 @@
1.74 serialiser = make_serialiser(format, output, linker, pagename)
1.75 outtext = serialise(d, serialiser)
1.76
1.77 + # Obtain a theme object for theming.
1.78 +
1.79 + theme = theme_name and make_theme("%s.%s" % (theme_name, format),
1.80 + output, linker, pagename)
1.81 +
1.82 + # With a theme, apply it to the text.
1.83 +
1.84 + if theme:
1.85 + outtext = theme.apply(outtext)
1.86 +
1.87 # If reading from a file, show the result. Otherwise, write to the
1.88 # output context.
1.89
1.90 @@ -232,6 +261,11 @@
1.91 output.writepage(outtext, pagename)
1.92 print >>sys.stderr, pagename
1.93
1.94 + # Install any theme resources.
1.95 +
1.96 + if theme:
1.97 + theme.install_resources()
1.98 +
1.99 if __name__ == "__main__":
1.100 main()
1.101
2.1 --- a/moinformat/__init__.py Tue Aug 07 23:45:24 2018 +0200
2.2 +++ b/moinformat/__init__.py Mon Aug 13 22:57:16 2018 +0200
2.3 @@ -24,5 +24,6 @@
2.4 from moinformat.output import make_output
2.5 from moinformat.parsers import get_parser, make_parser, parse
2.6 from moinformat.serialisers import get_serialiser, make_serialiser, serialise
2.7 +from moinformat.themes import make_theme
2.8
2.9 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/moinformat/links/__init__.py Tue Aug 07 23:45:24 2018 +0200
3.2 +++ b/moinformat/links/__init__.py Mon Aug 13 22:57:16 2018 +0200
3.3 @@ -32,17 +32,17 @@
3.4
3.5 return linkers.get(name)
3.6
3.7 -def make_linker(name, pagename, mapping=None):
3.8 +def make_linker(name, pagename, mapping=None, parameters=None):
3.9
3.10 """
3.11 Return a linking scheme handler with the given 'name' and using the given
3.12 - 'pagename' and interwiki 'mapping'.
3.13 + 'pagename', interwiki 'mapping' and 'parameters'.
3.14 """
3.15
3.16 linker_cls = get_linker(name)
3.17 if not linker_cls:
3.18 return None
3.19
3.20 - return linker_cls(pagename, mapping)
3.21 + return linker_cls(pagename, mapping, parameters)
3.22
3.23 # vim: tabstop=4 expandtab shiftwidth=4
4.1 --- a/moinformat/links/common.py Tue Aug 07 23:45:24 2018 +0200
4.2 +++ b/moinformat/links/common.py Mon Aug 13 22:57:16 2018 +0200
4.3 @@ -23,14 +23,64 @@
4.4
4.5 "Translate Moin links into other forms."
4.6
4.7 - def __init__(self, pagename, mapping=None):
4.8 + def __init__(self, pagename, mapping=None, parameters=None):
4.9
4.10 """
4.11 - Initialise the linker with the 'pagename' and optional interwiki
4.12 - 'mapping'.
4.13 + Initialise the linker with the 'pagename', optional interwiki 'mapping'
4.14 + and 'parameters'.
4.15 """
4.16
4.17 self.pagename = pagename
4.18 self.mapping = mapping or {}
4.19 + self.parameters = parameters or {}
4.20 +
4.21 + self.root_pagename = self.parameters.get("root_pagename") or "FrontPage"
4.22 +
4.23 +def resolve(path, pagename, root_pagename):
4.24 +
4.25 + "Resolve 'path' relative to 'pagename'."
4.26 +
4.27 + # Omit the root pagename from the resolved path components.
4.28 +
4.29 + if pagename == root_pagename:
4.30 + parts = []
4.31 + else:
4.32 + parts = pagename.rstrip("/").split("/")
4.33 +
4.34 + t = path.split("/")
4.35 +
4.36 + first = True
4.37 +
4.38 + for p in t:
4.39 +
4.40 + # Handle replacement of the page with another.
4.41 +
4.42 + if p == ".":
4.43 + parts = []
4.44 +
4.45 + # Handle ascent in the page hierarchy.
4.46 +
4.47 + elif p == "..":
4.48 + if parts:
4.49 + parts.pop()
4.50 +
4.51 + # Any non-navigation element replaces the path at the start.
4.52 + # Otherwise, the path is extended.
4.53 + # Omit the root pagename from the resolved path components if it would
4.54 + # appear at the start.
4.55 +
4.56 + elif p:
4.57 + if first:
4.58 + if p == root_pagename:
4.59 + parts = []
4.60 + else:
4.61 + parts = [p]
4.62 + else:
4.63 + if parts or p != root_pagename:
4.64 + parts.append(p)
4.65 +
4.66 + first = False
4.67 +
4.68 + return "/".join(parts)
4.69
4.70 # vim: tabstop=4 expandtab shiftwidth=4
5.1 --- a/moinformat/links/html.py Tue Aug 07 23:45:24 2018 +0200
5.2 +++ b/moinformat/links/html.py Mon Aug 13 22:57:16 2018 +0200
5.3 @@ -19,7 +19,7 @@
5.4 this program. If not, see <http://www.gnu.org/licenses/>.
5.5 """
5.6
5.7 -from moinformat.links.common import Linker
5.8 +from moinformat.links.common import Linker, resolve
5.9 from urllib import quote, quote_plus
5.10 from urlparse import urlparse
5.11
5.12 @@ -33,7 +33,14 @@
5.13
5.14 "Return a relative link to the top level."
5.15
5.16 - levels = self.pagename.count("/")
5.17 + # The root page is at the top level already.
5.18 +
5.19 + if self.pagename == self.root_pagename:
5.20 + return ""
5.21 +
5.22 + # Siblings of the root page are actually one level below.
5.23 +
5.24 + levels = self.pagename.count("/") + 1
5.25 return "/".join([".."] * levels)
5.26
5.27 def is_url(self, target):
5.28 @@ -58,20 +65,20 @@
5.29
5.30 target = target.rstrip("/")
5.31
5.32 - # Fragments.
5.33 + # Fragments. Remove the leading hash for the label.
5.34
5.35 if target.startswith("#"):
5.36 - return self.quote(target), None
5.37 + return self.quote(target), target.lstrip("#")
5.38
5.39 # Sub-pages. Remove the leading slash for the label.
5.40
5.41 - elif target.startswith("/"):
5.42 - return self.translate_subpage(target), target.lstrip("/")
5.43 + if target.startswith("/"):
5.44 + return self.translate_pagename(target), target.lstrip("/")
5.45
5.46 # Sibling (of ancestor) pages.
5.47
5.48 if target.startswith("../"):
5.49 - return self.translate_relative(target), None
5.50 + return self.translate_pagename(target), None
5.51
5.52 # Attachment or interwiki link.
5.53
5.54 @@ -87,8 +94,30 @@
5.55
5.56 # Top-level pages.
5.57
5.58 + return self.translate_pagename(target), None
5.59 +
5.60 + def translate_pagename(self, target):
5.61 +
5.62 + "Translate the pagename in 'target'."
5.63 +
5.64 + # Obtain the target pagename and the fragment.
5.65 + # Split the pagename into path components.
5.66 +
5.67 + t = target.split("#", 1)
5.68 + p = t[0].rstrip("/").split("/")
5.69 +
5.70 + # Determine the actual pagename referenced.
5.71 + # Replace the root pagename if it appears.
5.72 +
5.73 + resolved = resolve(t[0], self.pagename, self.root_pagename)
5.74 +
5.75 + # Rewrite the target using a relative link to the top level and then the
5.76 + # resolved pagename.
5.77 +
5.78 top_level = self.get_top_level()
5.79 - return self.quote("%s%s" % (top_level and "%s/" % top_level or "", target)), None
5.80 + t[0] = "%s%s" % (top_level and "%s/" % top_level or "", resolved)
5.81 +
5.82 + return self.quote("#".join(t))
5.83
5.84 def translate_qualified_link(self, target):
5.85
5.86 @@ -132,18 +161,6 @@
5.87
5.88 return "%s%s" % (self.normalise(url), self.quote(target))
5.89
5.90 - def translate_relative(self, target):
5.91 -
5.92 - "Return a translation of the given relative 'target'."
5.93 -
5.94 - return self.quote(target)
5.95 -
5.96 - def translate_subpage(self, target):
5.97 -
5.98 - "Return a translation of the given subpage 'target'."
5.99 -
5.100 - return self.quote(".%s" % target)
5.101 -
5.102 # Path encoding.
5.103
5.104 def quote(self, s):
6.1 --- a/moinformat/macros/toc.py Tue Aug 07 23:45:24 2018 +0200
6.2 +++ b/moinformat/macros/toc.py Mon Aug 13 22:57:16 2018 +0200
6.3 @@ -20,7 +20,28 @@
6.4 """
6.5
6.6 from moinformat.macros.common import Macro
6.7 -from moinformat.tree.moin import Container, Heading, Link, List, ListItem, Text
6.8 +from moinformat.tree.moin import Block, Container, Heading, Link, List, \
6.9 + ListItem, Text
6.10 +
6.11 +def in_range(min_level, level, max_level):
6.12 +
6.13 + """
6.14 + Test that 'min_level' <= 'level' <= 'max_level', only imposing tests
6.15 + involving limits not set to None.
6.16 + """
6.17 +
6.18 + return (min_level is None or min_level <= level) and \
6.19 + (max_level is None or level <= max_level)
6.20 +
6.21 +def above_minimum(min_level, level, max_level):
6.22 +
6.23 + """
6.24 + Test that 'min_level' < 'level' <= 'max_level', only imposing tests
6.25 + involving limits not set to None.
6.26 + """
6.27 +
6.28 + return (min_level is None or min_level < level) and \
6.29 + (max_level is None or level <= max_level)
6.30
6.31 class TableOfContents(Macro):
6.32
6.33 @@ -81,7 +102,7 @@
6.34
6.35 # Ignore levels outside the range of interest.
6.36
6.37 - if not (min_level <= level <= max_level):
6.38 + if not in_range(min_level, level, max_level):
6.39 continue
6.40
6.41 # Determine whether the heading should be generated at this
6.42 @@ -133,7 +154,7 @@
6.43
6.44 # Retain a list at the minimum level.
6.45
6.46 - if min_level < level <= max_level:
6.47 + if above_minimum(min_level, level, max_level):
6.48 lists.pop()
6.49
6.50 level -= 1
6.51 @@ -145,17 +166,52 @@
6.52
6.53 # Add the heading as an item.
6.54
6.55 - if min_level <= level <= max_level:
6.56 + if in_range(min_level, level, max_level):
6.57 +
6.58 indent = level - 1
6.59 nodes = self.get_entry(heading)
6.60
6.61 item = ListItem(nodes, indent, marker, space, None)
6.62 items.append(item)
6.63
6.64 - # Replace the macro node's children with the top-level list.
6.65 - # The macro cannot be replaced because it will be appearing inline.
6.66 + # Replace the macro node with the top-level list.
6.67 +
6.68 + self.insert_table(lists[0])
6.69 +
6.70 + def insert_table(self, content):
6.71 +
6.72 + "Insert the given 'content' into the document."
6.73 +
6.74 + macro = self.node
6.75 + parent = macro.parent
6.76 + region = macro.region
6.77 +
6.78 + # Replace the macro if it is not inside a block.
6.79 + # NOTE: This attempts to avoid blocks being used in inline-only contexts
6.80 + # NOTE: but may not be successful in every case.
6.81 +
6.82 + if not isinstance(parent, Block) or parent is region:
6.83 + parent.replace(macro, content)
6.84
6.85 - self.node.nodes = lists and [lists[0]] or []
6.86 + # Split any block containing the macro into preceding and following
6.87 + # parts.
6.88 +
6.89 + else:
6.90 + following = parent.split_at(macro)
6.91 +
6.92 + # Insert any non-empty following block.
6.93 +
6.94 + if not following.whitespace_only():
6.95 + region.insert_after(parent, following)
6.96 +
6.97 + # Insert the new content.
6.98 +
6.99 + region.insert_after(parent, content)
6.100 +
6.101 + # Remove any empty preceding block.
6.102 +
6.103 + if parent.whitespace_only():
6.104 + region.remove(parent)
6.105
6.106 def find_headings(self, node, headings):
6.107
7.1 --- a/moinformat/parsers/moin.py Tue Aug 07 23:45:24 2018 +0200
7.2 +++ b/moinformat/parsers/moin.py Mon Aug 13 22:57:16 2018 +0200
7.3 @@ -559,7 +559,7 @@
7.4 # interpret the individual arguments.
7.5
7.6 arglist = args and args.split(",") or []
7.7 - macro = Macro(name, arglist, region.append_point())
7.8 + macro = Macro(name, arglist, region.append_point(), region)
7.9 region.append_inline(macro)
7.10
7.11 # Record the macro for later processing.
8.1 --- a/moinformat/serialisers/manifest.py Tue Aug 07 23:45:24 2018 +0200
8.2 +++ b/moinformat/serialisers/manifest.py Mon Aug 13 22:57:16 2018 +0200
8.3 @@ -27,8 +27,8 @@
8.4
8.5 # Obtain all serialisers.
8.6
8.7 -# Use names declared in each class to register the handlers:
8.8 -# serialiser.format -> serialiser
8.9 +# Use module paths to register the handlers:
8.10 +# output_format.input_format -> serialiser
8.11
8.12 serialisers = get_mapping(modules, lambda n, m: n, lambda m: m.serialiser)
8.13
9.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
9.2 +++ b/moinformat/themes/__init__.py Mon Aug 13 22:57:16 2018 +0200
9.3 @@ -0,0 +1,48 @@
9.4 +#!/usr/bin/env python
9.5 +
9.6 +"""
9.7 +Theming support.
9.8 +
9.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
9.10 +
9.11 +This program is free software; you can redistribute it and/or modify it under
9.12 +the terms of the GNU General Public License as published by the Free Software
9.13 +Foundation; either version 3 of the License, or (at your option) any later
9.14 +version.
9.15 +
9.16 +This program is distributed in the hope that it will be useful, but WITHOUT
9.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
9.19 +details.
9.20 +
9.21 +You should have received a copy of the GNU General Public License along with
9.22 +this program. If not, see <http://www.gnu.org/licenses/>.
9.23 +"""
9.24 +
9.25 +from moinformat.themes.manifest import themes
9.26 +
9.27 +# Top-level functions.
9.28 +
9.29 +def get_theme(name):
9.30 +
9.31 + """
9.32 + Return the theme class with the given 'name' or None if no such class is
9.33 + found.
9.34 + """
9.35 +
9.36 + return themes.get(name)
9.37 +
9.38 +def make_theme(name, output, linker, pagename):
9.39 +
9.40 + """
9.41 + Return a theme of the type indicated by 'name', employing the given 'output'
9.42 + context, 'linker' and 'pagename'.
9.43 + """
9.44 +
9.45 + theme_cls = get_theme(name)
9.46 + if not theme_cls:
9.47 + return None
9.48 +
9.49 + return theme_cls(output, linker, pagename)
9.50 +
9.51 +# vim: tabstop=4 expandtab shiftwidth=4
10.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
10.2 +++ b/moinformat/themes/common.py Mon Aug 13 22:57:16 2018 +0200
10.3 @@ -0,0 +1,97 @@
10.4 +#!/usr/bin/env python
10.5 +
10.6 +"""
10.7 +Theming common functionality.
10.8 +
10.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
10.10 +
10.11 +This program is free software; you can redistribute it and/or modify it under
10.12 +the terms of the GNU General Public License as published by the Free Software
10.13 +Foundation; either version 3 of the License, or (at your option) any later
10.14 +version.
10.15 +
10.16 +This program is distributed in the hope that it will be useful, but WITHOUT
10.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
10.19 +details.
10.20 +
10.21 +You should have received a copy of the GNU General Public License along with
10.22 +this program. If not, see <http://www.gnu.org/licenses/>.
10.23 +"""
10.24 +
10.25 +from os import listdir, makedirs
10.26 +from os.path import exists, isfile, join, split
10.27 +from shutil import copy
10.28 +
10.29 +class Theme:
10.30 +
10.31 + "A common theme abstraction."
10.32 +
10.33 + def __init__(self, output, linker, pagename):
10.34 +
10.35 + """
10.36 + Initialise the theme with the given 'output' context, 'linker' and
10.37 + 'pagename'.
10.38 + """
10.39 +
10.40 + self.output = output
10.41 + self.linker = linker
10.42 + self.pagename = pagename
10.43 +
10.44 + def apply(self, text):
10.45 +
10.46 + "Apply this theme to the given 'text', returning a themed version."
10.47 +
10.48 + return text
10.49 +
10.50 + def get_resource_base(self):
10.51 +
10.52 + "Return the filesystem base of resources for instances of this class."
10.53 +
10.54 + return split(self.__class__.origin)[0]
10.55 +
10.56 + def get_resource(self, filename):
10.57 +
10.58 + "Return the complete path for the resource with the given 'filename'."
10.59 +
10.60 + base = self.get_resource_base()
10.61 + return join(base, filename)
10.62 +
10.63 + def install_resource(self, filename, target=None):
10.64 +
10.65 + """
10.66 + Install the resource with the given 'filename' into a location having
10.67 + the given 'target' name (or 'filename' if 'target' is omitted).
10.68 + """
10.69 +
10.70 + pathname = self.get_resource(filename)
10.71 + outpath = self.output.get_filename(target or filename)
10.72 +
10.73 + self.copy(pathname, outpath)
10.74 +
10.75 + def copy(self, pathname, outpath):
10.76 +
10.77 + "Copy 'pathname' to 'outpath'."
10.78 +
10.79 + if isfile(pathname):
10.80 + outdir = split(outpath)[0]
10.81 + if outdir and not exists(outdir):
10.82 + makedirs(outdir)
10.83 + copy(pathname, outpath)
10.84 + else:
10.85 + if not exists(outpath):
10.86 + makedirs(outpath)
10.87 + for filename in listdir(pathname):
10.88 + self.copy(join(pathname, filename), join(outpath, filename))
10.89 +
10.90 + def load_resource(self, filename):
10.91 +
10.92 + "Return the textual content of the resource with the given 'filename'."
10.93 +
10.94 + f = open(self.get_resource(filename))
10.95 + try:
10.96 + return f.read()
10.97 + finally:
10.98 + f.close()
10.99 +
10.100 +# vim: tabstop=4 expandtab shiftwidth=4
11.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
11.2 +++ b/moinformat/themes/default/__init__.py Mon Aug 13 22:57:16 2018 +0200
11.3 @@ -0,0 +1,22 @@
11.4 +#!/usr/bin/env python
11.5 +
11.6 +"""
11.7 +A default theme.
11.8 +
11.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
11.10 +
11.11 +This program is free software; you can redistribute it and/or modify it under
11.12 +the terms of the GNU General Public License as published by the Free Software
11.13 +Foundation; either version 3 of the License, or (at your option) any later
11.14 +version.
11.15 +
11.16 +This program is distributed in the hope that it will be useful, but WITHOUT
11.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
11.19 +details.
11.20 +
11.21 +You should have received a copy of the GNU General Public License along with
11.22 +this program. If not, see <http://www.gnu.org/licenses/>.
11.23 +"""
11.24 +
11.25 +# vim: tabstop=4 expandtab shiftwidth=4
12.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
12.2 +++ b/moinformat/themes/default/css/common.css Mon Aug 13 22:57:16 2018 +0200
12.3 @@ -0,0 +1,9 @@
12.4 +table {
12.5 + border-collapse: collapse;
12.6 + margin: 0.5em 0 0.5em 0;
12.7 +}
12.8 +
12.9 +table td {
12.10 + border: 1px solid #000;
12.11 + padding: 0.5em;
12.12 +}
13.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
13.2 +++ b/moinformat/themes/default/html.py Mon Aug 13 22:57:16 2018 +0200
13.3 @@ -0,0 +1,52 @@
13.4 +#!/usr/bin/env python
13.5 +
13.6 +"""
13.7 +A default theme for HTML output.
13.8 +
13.9 +Copyright (C) 2018 Paul Boddie <paul@boddie.org.uk>
13.10 +
13.11 +This program is free software; you can redistribute it and/or modify it under
13.12 +the terms of the GNU General Public License as published by the Free Software
13.13 +Foundation; either version 3 of the License, or (at your option) any later
13.14 +version.
13.15 +
13.16 +This program is distributed in the hope that it will be useful, but WITHOUT
13.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13.19 +details.
13.20 +
13.21 +You should have received a copy of the GNU General Public License along with
13.22 +this program. If not, see <http://www.gnu.org/licenses/>.
13.23 +"""
13.24 +
13.25 +from moinformat.themes.common import Theme
13.26 +
13.27 +class DefaultHTMLTheme(Theme):
13.28 +
13.29 + "A default theme."
13.30 +
13.31 + name = "html"
13.32 + origin = __file__
13.33 +
13.34 + def apply(self, text):
13.35 +
13.36 + "Apply this theme to the given 'text', returning a themed version."
13.37 +
13.38 + template = self.load_resource("template.html")
13.39 + subs = {
13.40 + "encoding" : self.output.encoding,
13.41 + "root" : self.linker.get_top_level() or ".",
13.42 + "text" : text,
13.43 + "title" : self.pagename,
13.44 + }
13.45 + return template % subs
13.46 +
13.47 + def install_resources(self):
13.48 +
13.49 + "Install resources for this theme."
13.50 +
13.51 + self.install_resource("css", "_css")
13.52 +
13.53 +theme = DefaultHTMLTheme
13.54 +
13.55 +# vim: tabstop=4 expandtab shiftwidth=4
14.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
14.2 +++ b/moinformat/themes/default/template.html Mon Aug 13 22:57:16 2018 +0200
14.3 @@ -0,0 +1,11 @@
14.4 +<!DOCTYPE html>
14.5 +<html>
14.6 +<head>
14.7 +<title>%(title)s</title>
14.8 +<link rel="stylesheet" type="text/css" charset="utf-8" media="all" href="%(root)s/_css/common.css" />
14.9 +<meta http-equiv="Content-Type" content="text/html;charset=%(encoding)s" />
14.10 +</head>
14.11 +<body>
14.12 +%(text)s
14.13 +</body>
14.14 +</html>
15.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
15.2 +++ b/moinformat/themes/manifest.py Mon Aug 13 22:57:16 2018 +0200
15.3 @@ -0,0 +1,35 @@
15.4 +#!/usr/bin/env python
15.5 +
15.6 +"""
15.7 +Theme implementation manifest.
15.8 +
15.9 +Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk>
15.10 +
15.11 +This program is free software; you can redistribute it and/or modify it under
15.12 +the terms of the GNU General Public License as published by the Free Software
15.13 +Foundation; either version 3 of the License, or (at your option) any later
15.14 +version.
15.15 +
15.16 +This program is distributed in the hope that it will be useful, but WITHOUT
15.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15.19 +details.
15.20 +
15.21 +You should have received a copy of the GNU General Public License along with
15.22 +this program. If not, see <http://www.gnu.org/licenses/>.
15.23 +"""
15.24 +
15.25 +from moinformat.imports import get_extensions, get_mapping, get_modules
15.26 +
15.27 +# Define an attribute mapping names to modules.
15.28 +
15.29 +modules = get_modules(__file__, __name__)
15.30 +
15.31 +# Obtain all themes.
15.32 +
15.33 +# Use module paths to register the contexts:
15.34 +# theme_name.output_format -> theme
15.35 +
15.36 +themes = get_mapping(modules, lambda n, m: n, lambda m: m.theme)
15.37 +
15.38 +# vim: tabstop=4 expandtab shiftwidth=4
16.1 --- a/moinformat/tree/moin.py Tue Aug 07 23:45:24 2018 +0200
16.2 +++ b/moinformat/tree/moin.py Mon Aug 13 22:57:16 2018 +0200
16.3 @@ -69,6 +69,13 @@
16.4 def empty(self):
16.5 return not self.nodes
16.6
16.7 + def insert_after(self, old, new):
16.8 +
16.9 + "Insert after 'old' in the children the 'new' node."
16.10 +
16.11 + index = self.nodes.index(old)
16.12 + self.nodes.insert(index + 1, new)
16.13 +
16.14 def node(self, index):
16.15 try:
16.16 return self.nodes[index]
16.17 @@ -106,6 +113,12 @@
16.18 if text:
16.19 self.append(text)
16.20
16.21 + def remove(self, node):
16.22 +
16.23 + "Remove 'node' from the children."
16.24 +
16.25 + self.nodes.remove(node)
16.26 +
16.27 def replace(self, old, new):
16.28
16.29 "Replace 'old' with 'new' in the children."
16.30 @@ -113,6 +126,21 @@
16.31 i = self.nodes.index(old)
16.32 self.nodes[i] = new
16.33
16.34 + def split_at(self, node):
16.35 +
16.36 + """
16.37 + Split the container at 'node', returning a new container holding the
16.38 + nodes following 'node' that are moved from this container.
16.39 + """
16.40 +
16.41 + i = self.nodes.index(node)
16.42 + following = self.__class__(self.nodes[i+1:])
16.43 +
16.44 + # Remove the node and the following parts from this container.
16.45 +
16.46 + del self.nodes[i:]
16.47 + return following
16.48 +
16.49 def text_content(self):
16.50
16.51 """
16.52 @@ -130,6 +158,12 @@
16.53
16.54 return "".join(l)
16.55
16.56 + def whitespace_only(self):
16.57 +
16.58 + "Return whether the container provides only whitespace text."
16.59 +
16.60 + return not self.text_content().strip()
16.61 +
16.62 def __str__(self):
16.63 return self.prettyprint()
16.64
16.65 @@ -508,14 +542,15 @@
16.66
16.67 "Macro details."
16.68
16.69 - def __init__(self, name, args, parent, nodes=None):
16.70 + def __init__(self, name, args, parent, region, nodes=None):
16.71 Container.__init__(self, nodes or [])
16.72 self.name = name
16.73 + self.args = args
16.74 self.parent = parent
16.75 - self.args = args
16.76 + self.region = region
16.77
16.78 def __repr__(self):
16.79 - return "Macro(%r, %r, %r, %r)" % (self.name, self.args, self.parent, self.nodes)
16.80 + return "Macro(%r, %r, %r, %r, %r)" % (self.name, self.args, self.parent, self.region, self.nodes)
16.81
16.82 def prettyprint(self, indent=""):
16.83 l = ["%sMacro: name=%r args=%r" % (indent, self.name, self.args)]
17.1 --- a/tests/test_macros.tree Tue Aug 07 23:45:24 2018 +0200
17.2 +++ b/tests/test_macros.tree Mon Aug 13 22:57:16 2018 +0200
17.3 @@ -8,6 +8,8 @@
17.4 Break
17.5 Block
17.6 Text
17.7 + Macro
17.8 + Text
17.9 Break
17.10 Heading
17.11 Text
19.1 --- a/tests/test_macros.txt Tue Aug 07 23:45:24 2018 +0200
19.2 +++ b/tests/test_macros.txt Mon Aug 13 22:57:16 2018 +0200
19.3 @@ -2,7 +2,7 @@
19.4
19.5 = Heading =
19.6
19.7 -Some text.
19.8 +Some text. <<TableOfContents(2)>> And more.
19.9
19.10 == Subheading ==
19.11
20.1 --- a/tests/test_parser.py Tue Aug 07 23:45:24 2018 +0200
20.2 +++ b/tests/test_parser.py Mon Aug 13 22:57:16 2018 +0200
20.3 @@ -1,5 +1,6 @@
20.4 #!/usr/bin/env python
20.5
20.6 +from os import listdir
20.7 from os.path import abspath, split
20.8 import sys
20.9
20.10 @@ -11,12 +12,12 @@
20.11 try:
20.12 import moinformat
20.13 except ImportError:
20.14 - if split(parent)[1] == "MoinLight":
20.15 + if "moinformat" in listdir(parent):
20.16 sys.path.append(parent)
20.17
20.18 # Import specific objects.
20.19
20.20 -from moinformat import make_input, make_output, make_serialiser, parse, serialise
20.21 +from moinformat import make_input, make_output, make_parser, make_serialiser, parse, serialise
20.22 from moinformat.tree.moin import Container
20.23
20.24 def test_input(d, s):
20.25 @@ -170,6 +171,16 @@
20.26
20.27 return branches[0]
20.28
20.29 +def get_tree(input, tree_filename):
20.30 +
20.31 + "Using 'input', return (text, tree) for 'tree_filename'."
20.32 +
20.33 + if input.dir.exists(tree_filename):
20.34 + ts = input.readfile(tree_filename)
20.35 + return ts, parse_tree(ts)
20.36 + else:
20.37 + return None, None
20.38 +
20.39 if __name__ == "__main__":
20.40 args = sys.argv[1:]
20.41
20.42 @@ -200,20 +211,20 @@
20.43 text_filename = filename
20.44 encoding = None
20.45
20.46 - tree_filename = "%s.tree" % text_filename.rsplit(".", 1)[0]
20.47 + basename = text_filename.rsplit(".", 1)[0]
20.48 + tree_filename = "%s.tree" % basename
20.49 + tree_exp_filename = "%s.tree-exp" % basename
20.50
20.51 # Read and parse the input.
20.52
20.53 s = input.readfile(text_filename, encoding)
20.54 - d = parse(s)
20.55 -
20.56 - # Read and parse any tree definition.
20.57 + p = make_parser()
20.58 + d = parse(s, p)
20.59
20.60 - if input.dir.exists(tree_filename):
20.61 - ts = input.readfile(tree_filename)
20.62 - t = parse_tree(ts)
20.63 - else:
20.64 - ts = None
20.65 + # Read and parse any tree definitions.
20.66 +
20.67 + ts, t = get_tree(input, tree_filename)
20.68 + tsexp, texp = get_tree(input, tree_exp_filename)
20.69
20.70 # Report the test results.
20.71
20.72 @@ -223,7 +234,13 @@
20.73 identical = test_input(d, s)
20.74 tree_identical = ts and test_tree(d, t, ts)
20.75
20.76 + if tsexp:
20.77 + p.evaluate_macros()
20.78 + tree_exp_identical = test_tree(d, texp, tsexp)
20.79 + else:
20.80 + tree_exp_identical = None
20.81 +
20.82 if quiet:
20.83 - print "%s %s: %s" % (identical, tree_identical, filename)
20.84 + print "%s %s %s: %s" % (identical, tree_identical, tree_exp_identical, filename)
20.85
20.86 # vim: tabstop=4 expandtab shiftwidth=4