Mercurial > wikked
changeset 83:65f83a9b42f1
Added support for numbered template parameters.
Moved resolver code into its own file.
Added unit tests.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 24 Mar 2013 22:06:50 -0700 |
parents | 9afe4a1dbd1e |
children | ca57fef14d04 |
files | tests/__init__.py tests/test_page.py tests/test_resolver.py wikked/formatter.py wikked/metautils.py wikked/page.py wikked/resolver.py |
diffstat | 7 files changed, 394 insertions(+), 370 deletions(-) [+] |
line wrap: on
line diff
--- a/tests/__init__.py Tue Mar 19 19:54:11 2013 -0700 +++ b/tests/__init__.py Sun Mar 24 22:06:50 2013 -0700 @@ -3,7 +3,7 @@ import shutil import unittest from wikked.wiki import Wiki -from mock import MockWikiParameters +from mock import MockWikiParameters, MockFileSystem class WikkedTest(unittest.TestCase): @@ -32,6 +32,11 @@ def getParameters(self): return MockWikiParameters() + def _getWikiFromStructure(self, structure): + wiki = self.getWiki(use_db=False, fs_factory=lambda cfg: MockFileSystem(structure)) + wiki.start() + return wiki + def format_link(title, url, missing=False, mod=None): res = '<a class=\"wiki-link'
--- a/tests/test_page.py Tue Mar 19 19:54:11 2013 -0700 +++ b/tests/test_page.py Sun Mar 24 22:06:50 2013 -0700 @@ -1,14 +1,8 @@ -from tests import WikkedTest, format_link, format_include -from mock import MockFileSystem +from tests import WikkedTest, format_link from wikked.page import Page class PageTest(WikkedTest): - def _getWikiFromStructure(self, structure): - wiki = self.getWiki(use_db=False, fs_factory=lambda cfg: MockFileSystem(structure)) - wiki.start() - return wiki - def testSimplePage(self): self.wiki = self._getWikiFromStructure({ 'foo.txt': 'A test page.' @@ -89,91 +83,3 @@ foo = Page(self.wiki, 'foo') self.assertEqual("URL: /files/blah/boo/image.png", foo._getFormattedText()) - def testPageInclude(self): - self.wiki = self._getWikiFromStructure({ - 'Foo.txt': "A test page.\n{{include: trans-desc}}\n", - 'Trans Desc.txt': "BLAH\n" - }) - foo = Page(self.wiki, 'foo') - self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta()) - self.assertEqual( - "A test page.\n%s" % format_include('trans-desc'), - foo._getFormattedText()) - self.assertEqual("A test page.\nBLAH\n\n", foo.text) - - def testPageIncludeWithMeta(self): - self.wiki = self._getWikiFromStructure({ - 'Foo.txt': "A test page.\n{{include: trans-desc}}\n", - 'Trans Desc.txt': "BLAH: [[Somewhere]]\n{{bar: 42}}\n{{__secret: love}}\n{{+given: hope}}" - }) - foo = Page(self.wiki, 'foo') - self.assertEqual([], foo._getLocalLinks()) - self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta()) - self.assertEqual( - "A test page.\n%s" % format_include('trans-desc'), - foo._getFormattedText()) - self.assertEqual( - "A test page.\nBLAH: %s\n\n\n\n" % format_link('Somewhere', 'somewhere', True), - foo.text) - self.assertEqual(['somewhere'], foo.links) - self.assertEqual({'bar': ['42'], 'given': ['hope'], 'include': ['trans-desc']}, foo.meta) - - def testPageIncludeWithTemplating(self): - self.wiki = self._getWikiFromStructure({ - 'Foo.txt': "A test page.\n{{include: greeting|name=Dave|what=drink}}\n", - 'Greeting.txt': "Hello {{name}}, would you like a {{what}}?" - }) - foo = Page(self.wiki, 'foo') - self.assertEqual( - "A test page.\n%s" % format_include('greeting', 'name=Dave|what=drink'), - foo._getFormattedText()) - self.assertEqual("A test page.\nHello Dave, would you like a drink?\n", foo.text) - - def testGivenOnlyInclude(self): - self.wiki = self._getWikiFromStructure({ - 'Base.txt': "The base page.\n{{include: Template 1}}", - 'Template 1.txt': "TEMPLATE!\n{{+include: Template 2}}", - 'Template 2.txt': "MORE TEMPLATE!" - }) - tpl1 = Page(self.wiki, 'template-1') - self.assertEqual( - "TEMPLATE!\n%s" % format_include('template-2', mod='+'), - tpl1._getFormattedText()) - self.assertEqual("TEMPLATE!\n\n", tpl1.text) - base = Page(self.wiki, 'base') - self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!\n\n", base.text) - - def testDoublePageIncludeWithMeta(self): - return - self.wiki = self._getWikiFromStructure({ - 'Base.txt': "The base page.\n{{include: Template 1}}", - 'Wrong.txt': "{{include: Template 2}}", - 'Template 1.txt': "{{foo: bar}}\n{{+category: blah}}\n{{+include: Template 2}}\n{{__secret1: ssh}}", - 'Template 2.txt': "{{+category: yolo}}", - 'Query 1.txt': "{{query: category=yolo}}", - 'Query 2.txt': "{{query: category=blah}}" - }) - base = Page(self.wiki, 'base') - self.assertEqual({ - 'foo': ['bar'], - 'category': ['blah', 'yolo'] - }, base.meta) - tpl1 = Page(self.wiki, 'template-1') - self.assertEqual({ - 'foo': ['bar'], - '+category': ['blah'], - '+include': ['template-2'], - '__secret': ['ssh'] - }, tpl1.meta) - self.assertEqual( - "\n\n%s\n\n" % format_include('template-2'), - tpl1.text) - q1 = Page(self.wiki, 'query-1') - self.assertEqual( - "<ul>\n<li>%s</li>\n<li>%s</li>\n</ul>" % (format_link('Base', 'base'), format_link('Wrong', 'wrong')), - q1.text) - q2 = Page(self.wiki, 'query-2') - self.assertEqual( - "<ul>\n<li>%s</li>\n</ul>" % format_link('Base', 'base'), - q2.text) -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_resolver.py Sun Mar 24 22:06:50 2013 -0700 @@ -0,0 +1,104 @@ +from tests import WikkedTest, format_link, format_include +from wikked.page import Page + + +class ResolverTest(WikkedTest): + def testPageInclude(self): + self.wiki = self._getWikiFromStructure({ + 'Foo.txt': "A test page.\n{{include: trans-desc}}\n", + 'Trans Desc.txt': "BLAH\n" + }) + foo = Page(self.wiki, 'foo') + self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta()) + self.assertEqual( + "A test page.\n%s" % format_include('trans-desc'), + foo._getFormattedText()) + self.assertEqual("A test page.\nBLAH\n\n", foo.text) + + def testPageIncludeWithMeta(self): + self.wiki = self._getWikiFromStructure({ + 'Foo.txt': "A test page.\n{{include: trans-desc}}\n", + 'Trans Desc.txt': "BLAH: [[Somewhere]]\n{{bar: 42}}\n{{__secret: love}}\n{{+given: hope}}" + }) + foo = Page(self.wiki, 'foo') + self.assertEqual([], foo._getLocalLinks()) + self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta()) + self.assertEqual( + "A test page.\n%s" % format_include('trans-desc'), + foo._getFormattedText()) + self.assertEqual( + "A test page.\nBLAH: %s\n\n\n\n" % format_link('Somewhere', 'somewhere', True), + foo.text) + self.assertEqual(['somewhere'], foo.links) + self.assertEqual({'bar': ['42'], 'given': ['hope'], 'include': ['trans-desc']}, foo.meta) + + def testPageIncludeWithNamedTemplating(self): + self.wiki = self._getWikiFromStructure({ + 'Foo.txt': "A test page.\n{{include: greeting|name=Dave|what=drink}}\n", + 'Greeting.txt': "Hello {{name}}, would you like a {{what}}?" + }) + foo = Page(self.wiki, 'foo') + self.assertEqual( + "A test page.\n%s" % format_include('greeting', 'name=Dave|what=drink'), + foo._getFormattedText()) + self.assertEqual("A test page.\nHello Dave, would you like a drink?\n", foo.text) + + def testPageIncludeWithNumberedTemplating(self): + self.wiki = self._getWikiFromStructure({ + 'Foo.txt': "A test page.\n{{include: greeting|Dave|Roger|Tom}}\n", + 'Greeting.txt': "Hello {{1}}, {{2}} and {{3}}." + }) + foo = Page(self.wiki, 'foo') + self.assertEqual( + "A test page.\n%s" % format_include('greeting', 'Dave|Roger|Tom'), + foo._getFormattedText()) + self.assertEqual("A test page.\nHello Dave, Roger and Tom.\n", foo.text) + + def testGivenOnlyInclude(self): + self.wiki = self._getWikiFromStructure({ + 'Base.txt': "The base page.\n{{include: Template 1}}", + 'Template 1.txt': "TEMPLATE!\n{{+include: Template 2}}", + 'Template 2.txt': "MORE TEMPLATE!" + }) + tpl1 = Page(self.wiki, 'template-1') + self.assertEqual( + "TEMPLATE!\n%s" % format_include('template-2', mod='+'), + tpl1._getFormattedText()) + self.assertEqual("TEMPLATE!\n\n", tpl1.text) + base = Page(self.wiki, 'base') + self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!\n\n", base.text) + + def testDoublePageIncludeWithMeta(self): + return + self.wiki = self._getWikiFromStructure({ + 'Base.txt': "The base page.\n{{include: Template 1}}", + 'Wrong.txt': "{{include: Template 2}}", + 'Template 1.txt': "{{foo: bar}}\n{{+category: blah}}\n{{+include: Template 2}}\n{{__secret1: ssh}}", + 'Template 2.txt': "{{+category: yolo}}", + 'Query 1.txt': "{{query: category=yolo}}", + 'Query 2.txt': "{{query: category=blah}}" + }) + base = Page(self.wiki, 'base') + self.assertEqual({ + 'foo': ['bar'], + 'category': ['blah', 'yolo'] + }, base.meta) + tpl1 = Page(self.wiki, 'template-1') + self.assertEqual({ + 'foo': ['bar'], + '+category': ['blah'], + '+include': ['template-2'], + '__secret': ['ssh'] + }, tpl1.meta) + self.assertEqual( + "\n\n%s\n\n" % format_include('template-2'), + tpl1.text) + q1 = Page(self.wiki, 'query-1') + self.assertEqual( + "<ul>\n<li>%s</li>\n<li>%s</li>\n</ul>" % (format_link('Base', 'base'), format_link('Wrong', 'wrong')), + q1.text) + q2 = Page(self.wiki, 'query-2') + self.assertEqual( + "<ul>\n<li>%s</li>\n</ul>" % format_link('Base', 'base'), + q2.text) +
--- a/wikked/formatter.py Tue Mar 19 19:54:11 2013 -0700 +++ b/wikked/formatter.py Sun Mar 24 22:06:50 2013 -0700 @@ -1,23 +1,7 @@ import os import os.path import re -import pystache - - -def get_meta_name_and_modifiers(name): - """ Strips a meta name from any leading modifiers like `__` or `+` - and returns both as a tuple. If no modifier was found, the - second tuple value is `None`. - """ - clean_name = name - modifiers = None - if name[:2] == '__': - modifiers = '__' - clean_name = name[3:] - elif name[0] == '+': - modifiers = '+' - clean_name = name[1:] - return (clean_name, modifiers) +from metautils import get_meta_name_and_modifiers class FormatterNotFound(Exception): @@ -27,15 +11,6 @@ pass -class CircularIncludeError(Exception): - """ An exception raised when a circular include is found - while rendering a page. - """ - def __init__(self, message, url_trail): - Exception.__init__(self, message) - self.url_trail = url_trail - - class BaseContext(object): """ Base context for formatting pages. """ def __init__(self, url, slugify=None): @@ -242,250 +217,3 @@ urls.append(str(m.group('url'))) return urls -class ResolveContext(object): - """ The context for resolving page queries. """ - def __init__(self, root_url=None): - self.url_trail = set() - if root_url: - self.url_trail.add(root_url) - - def shouldRunMeta(self, modifier): - if modifier is None: - return True - if modifier == '__': - return len(self.url_trail) <= 1 - if modifier == '+': - return len(self.url_trail) > 1 - raise ValueError("Unknown modifier: " + modifier) - - -class ResolveOutput(object): - """ The results of a resolve operation. """ - def __init__(self, page=None): - self.text = '' - self.meta = {} - self.out_links = [] - if page: - self.meta = dict(page._getLocalMeta()) - self.out_links = list(page._getLocalLinks()) - - def add(self, other): - self.out_links += other.out_links - for original_key, val in other.meta.iteritems(): - # Ignore internal properties. Strip include-only properties - # from their prefix. - key, mod = get_meta_name_and_modifiers(original_key) - if mod == '__': - continue - - if key not in self.meta: - self.meta[key] = val - else: - self.meta[key].append(val) - - -class PageResolver(object): - """ An object responsible for resolving page queries like - `include` or `query`. - """ - default_parameters = { - '__header': "<ul>\n", - '__footer': "</ul>\n", - '__item': "<li><a class=\"wiki-link\" data-wiki-url=\"{{url}}\">" + - "{{title}}</a></li>\n", - '__empty': "<p>No page matches the query.</p>\n" - } - - def __init__(self, page, ctx=None): - self.page = page - self.ctx = ctx - self.output = None - - @property - def wiki(self): - return self.page.wiki - - def run(self): - if not self.ctx: - self.ctx = ResolveContext(self.page.url) - - # Resolve link states. - def repl1(m): - url = str(m.group('url')) - if self.wiki.pageExists(url): - return str(m.group()) - return '<a class="wiki-link missing" data-wiki-url="%s">' % url - - formatted_text = re.sub( - r'<a class="wiki-link" data-wiki-url="(?P<url>[^"]+)">', - repl1, - self.page._getFormattedText()) - - # Resolve queries, includes, etc. - def repl2(m): - meta_name = str(m.group('name')) - meta_value = str(m.group('value')) - meta_opts = {} - if m.group('opts'): - for c in re.finditer( - r'data-wiki-(?P<name>[a-z]+)="(?P<value>[^"]+)"', - str(m.group('opts'))): - opt_name = str(c.group('name')) - opt_value = str(c.group('value')) - meta_opts[opt_name] = opt_value - - if meta_name == 'query': - return self._runQuery(meta_opts, meta_value) - elif meta_name == 'include': - return self._runInclude(meta_opts, meta_value) - return '' - - self.output = ResolveOutput(self.page) - self.output.text = re.sub( - r'^<div class="wiki-(?P<name>[a-z]+)"' - r'(?P<opts>( data-wiki-([a-z]+)="([^"]+)")*)' - r'>(?P<value>.*)</div>$', - repl2, - formatted_text, - flags=re.MULTILINE) - - return self.output - - def _runInclude(self, opts, args): - # Should we even run this include? - if 'mod' in opts: - if not self.ctx.shouldRunMeta(opts['mod']): - return '' - - # Check for circular includes. - include_url = opts['url'] - if include_url in self.ctx.url_trail: - raise CircularIncludeError("Circular include detected at: %s" % include_url, self.ctx.url_trail) - - # Parse the templating parameters. - parameters = None - if args: - parameters = {} - arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*=(?P<value>[^\|]+)" - for m in re.finditer(arg_pattern, args): - key = str(m.group('name')).lower() - parameters[key] = m.group('value').strip() - - # Re-run the resolver on the included page to get its final - # formatted text. - page = self.wiki.getPage(include_url) - self.ctx.url_trail.add(page.url) - child = PageResolver(page, self.ctx) - child_output = child.run() - self.output.add(child_output) - - # Run some simple templating if we need to. - text = child_output.text - if parameters: - text = self._renderTemplate(text, parameters) - - return text - - def _runQuery(self, opts, query): - # Should we even run this query? - if 'mod' in opts: - if not self.ctx.shouldRunMeta(opts['mod']): - return '' - - # Parse the query. - parameters = dict(self.default_parameters) - meta_query = {} - arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*="\ - r"(?P<value>[^\|]+)" - for m in re.finditer(arg_pattern, query): - key = m.group('name').lower() - if key in parameters: - parameters[key] = str(m.group('value')) - else: - meta_query[key] = str(m.group('value')) - - # Find pages that match the query, excluding any page - # that is in the URL trail. - matched_pages = [] - for p in self.wiki.getPages(): - if p.url in self.ctx.url_trail: - continue - for key, value in meta_query.iteritems(): - if self._isPageMatch(p, key, value): - matched_pages.append(p) - - # No match: return the 'empty' template. - if len(matched_pages) == 0: - return self._valueOrPageText(parameters['__empty']) - - # Combine normal templates to build the output. - text = self._valueOrPageText(parameters['__header']) - for p in matched_pages: - tokens = { - 'url': p.url, - 'title': p.title - } - tokens.update(p._getLocalMeta()) - text += self._renderTemplate( - self._valueOrPageText(parameters['__item']), - tokens) - text += self._valueOrPageText(parameters['__footer']) - - return text - - def _valueOrPageText(self, value): - if re.match(r'^\[\[.*\]\]$', value): - page = self.wiki.getPage(value[2:-2]) - return page.text - return value - - def _isPageMatch(self, page, name, value, level=0): - # Check the page's local meta properties. - meta_keys = [name] - if level > 0: - # If this is an include, also look for 'include-only' - # meta properties. - meta_keys.append('+' + name) - for key in meta_keys: - actual = page._getLocalMeta().get(key) - if (actual is not None and - ((type(actual) is list and value in actual) or - (actual == value))): - return True - - # Gather included pages' URLs. - # If this is an include, also look for `+include`'d pages, - # and if not, `__include`'d pages. - include_meta_values = [] - include_meta_keys = ['include'] - if level > 0: - include_meta_keys.append('+include') - else: - include_meta_keys.append('__include') - for key in include_meta_keys: - i = page._getLocalMeta().get(key) - if i is not None: - if (type(i) is list): - include_meta_values += i - else: - include_meta_values.append(i) - included_urls = [] - for v in include_meta_values: - pipe_idx = v.find('|') - if pipe_idx > 0: - included_urls.append(v[:pipe_idx]) - else: - included_urls.append(v) - - # Recurse into included pages. - for url in included_urls: - p = self.wiki.getPage(url) - if self._isPageMatch(p, name, value, level + 1): - return True - - return False - - def _renderTemplate(self, text, parameters): - renderer = pystache.Renderer(search_dirs=[]) - return renderer.render(text, parameters) -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/metautils.py Sun Mar 24 22:06:50 2013 -0700 @@ -0,0 +1,17 @@ + +def get_meta_name_and_modifiers(name): + """ Strips a meta name from any leading modifiers like `__` or `+` + and returns both as a tuple. If no modifier was found, the + second tuple value is `None`. + """ + clean_name = name + modifiers = None + if name[:2] == '__': + modifiers = '__' + clean_name = name[3:] + elif name[0] == '+': + modifiers = '+' + clean_name = name[1:] + return (clean_name, modifiers) + +
--- a/wikked/page.py Tue Mar 19 19:54:11 2013 -0700 +++ b/wikked/page.py Sun Mar 24 22:06:50 2013 -0700 @@ -4,7 +4,8 @@ import datetime import unicodedata import pystache -from formatter import PageFormatter, FormattingContext, PageResolver, CircularIncludeError +from formatter import PageFormatter, FormattingContext +from resolver import PageResolver, CircularIncludeError class PageData(object):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/resolver.py Sun Mar 24 22:06:50 2013 -0700 @@ -0,0 +1,263 @@ +import re +import pystache +from metautils import get_meta_name_and_modifiers + + +class CircularIncludeError(Exception): + """ An exception raised when a circular include is found + while rendering a page. + """ + def __init__(self, message, url_trail): + Exception.__init__(self, message) + self.url_trail = url_trail + + +class ResolveContext(object): + """ The context for resolving page queries. """ + def __init__(self, root_url=None): + self.url_trail = set() + if root_url: + self.url_trail.add(root_url) + + def shouldRunMeta(self, modifier): + if modifier is None: + return True + if modifier == '__': + return len(self.url_trail) <= 1 + if modifier == '+': + return len(self.url_trail) > 1 + raise ValueError("Unknown modifier: " + modifier) + + +class ResolveOutput(object): + """ The results of a resolve operation. """ + def __init__(self, page=None): + self.text = '' + self.meta = {} + self.out_links = [] + if page: + self.meta = dict(page._getLocalMeta()) + self.out_links = list(page._getLocalLinks()) + + def add(self, other): + self.out_links += other.out_links + for original_key, val in other.meta.iteritems(): + # Ignore internal properties. Strip include-only properties + # from their prefix. + key, mod = get_meta_name_and_modifiers(original_key) + if mod == '__': + continue + + if key not in self.meta: + self.meta[key] = val + else: + self.meta[key].append(val) + + +class PageResolver(object): + """ An object responsible for resolving page queries like + `include` or `query`. + """ + default_parameters = { + '__header': "<ul>\n", + '__footer': "</ul>\n", + '__item': "<li><a class=\"wiki-link\" data-wiki-url=\"{{url}}\">" + + "{{title}}</a></li>\n", + '__empty': "<p>No page matches the query.</p>\n" + } + + def __init__(self, page, ctx=None): + self.page = page + self.ctx = ctx + self.output = None + + @property + def wiki(self): + return self.page.wiki + + def run(self): + if not self.ctx: + self.ctx = ResolveContext(self.page.url) + + # Resolve link states. + def repl1(m): + url = str(m.group('url')) + if self.wiki.pageExists(url): + return str(m.group()) + return '<a class="wiki-link missing" data-wiki-url="%s">' % url + + formatted_text = re.sub( + r'<a class="wiki-link" data-wiki-url="(?P<url>[^"]+)">', + repl1, + self.page._getFormattedText()) + + # Resolve queries, includes, etc. + def repl2(m): + meta_name = str(m.group('name')) + meta_value = str(m.group('value')) + meta_opts = {} + if m.group('opts'): + for c in re.finditer( + r'data-wiki-(?P<name>[a-z]+)="(?P<value>[^"]+)"', + str(m.group('opts'))): + opt_name = str(c.group('name')) + opt_value = str(c.group('value')) + meta_opts[opt_name] = opt_value + + if meta_name == 'query': + return self._runQuery(meta_opts, meta_value) + elif meta_name == 'include': + return self._runInclude(meta_opts, meta_value) + return '' + + self.output = ResolveOutput(self.page) + self.output.text = re.sub( + r'^<div class="wiki-(?P<name>[a-z]+)"' + r'(?P<opts>( data-wiki-([a-z]+)="([^"]+)")*)' + r'>(?P<value>.*)</div>$', + repl2, + formatted_text, + flags=re.MULTILINE) + + return self.output + + def _runInclude(self, opts, args): + # Should we even run this include? + if 'mod' in opts: + if not self.ctx.shouldRunMeta(opts['mod']): + return '' + + # Check for circular includes. + include_url = opts['url'] + if include_url in self.ctx.url_trail: + raise CircularIncludeError("Circular include detected at: %s" % include_url, self.ctx.url_trail) + + # Parse the templating parameters. + parameters = None + if args: + parameters = {} + arg_pattern = r"(^|\|)\s*((?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*=)?(?P<value>[^\|]+)" + for i, m in enumerate(re.finditer(arg_pattern, args)): + key = str(m.group('name')).lower() + value = str(m.group('value')).strip() + parameters[key] = value + parameters[str(i + 1)] = value + + # Re-run the resolver on the included page to get its final + # formatted text. + page = self.wiki.getPage(include_url) + self.ctx.url_trail.add(page.url) + child = PageResolver(page, self.ctx) + child_output = child.run() + self.output.add(child_output) + + # Run some simple templating if we need to. + text = child_output.text + if parameters: + text = self._renderTemplate(text, parameters) + + return text + + def _runQuery(self, opts, query): + # Should we even run this query? + if 'mod' in opts: + if not self.ctx.shouldRunMeta(opts['mod']): + return '' + + # Parse the query. + parameters = dict(self.default_parameters) + meta_query = {} + arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*="\ + r"(?P<value>[^\|]+)" + for m in re.finditer(arg_pattern, query): + key = m.group('name').lower() + if key in parameters: + parameters[key] = str(m.group('value')) + else: + meta_query[key] = str(m.group('value')) + + # Find pages that match the query, excluding any page + # that is in the URL trail. + matched_pages = [] + for p in self.wiki.getPages(): + if p.url in self.ctx.url_trail: + continue + for key, value in meta_query.iteritems(): + if self._isPageMatch(p, key, value): + matched_pages.append(p) + + # No match: return the 'empty' template. + if len(matched_pages) == 0: + return self._valueOrPageText(parameters['__empty']) + + # Combine normal templates to build the output. + text = self._valueOrPageText(parameters['__header']) + for p in matched_pages: + tokens = { + 'url': p.url, + 'title': p.title + } + tokens.update(p._getLocalMeta()) + text += self._renderTemplate( + self._valueOrPageText(parameters['__item']), + tokens) + text += self._valueOrPageText(parameters['__footer']) + + return text + + def _valueOrPageText(self, value): + if re.match(r'^\[\[.*\]\]$', value): + page = self.wiki.getPage(value[2:-2]) + return page.text + return value + + def _isPageMatch(self, page, name, value, level=0): + # Check the page's local meta properties. + meta_keys = [name] + if level > 0: + # If this is an include, also look for 'include-only' + # meta properties. + meta_keys.append('+' + name) + for key in meta_keys: + actual = page._getLocalMeta().get(key) + if (actual is not None and + ((type(actual) is list and value in actual) or + (actual == value))): + return True + + # Gather included pages' URLs. + # If this is an include, also look for `+include`'d pages, + # and if not, `__include`'d pages. + include_meta_values = [] + include_meta_keys = ['include'] + if level > 0: + include_meta_keys.append('+include') + else: + include_meta_keys.append('__include') + for key in include_meta_keys: + i = page._getLocalMeta().get(key) + if i is not None: + if (type(i) is list): + include_meta_values += i + else: + include_meta_values.append(i) + included_urls = [] + for v in include_meta_values: + pipe_idx = v.find('|') + if pipe_idx > 0: + included_urls.append(v[:pipe_idx]) + else: + included_urls.append(v) + + # Recurse into included pages. + for url in included_urls: + p = self.wiki.getPage(url) + if self._isPageMatch(p, name, value, level + 1): + return True + + return False + + def _renderTemplate(self, text, parameters): + renderer = pystache.Renderer(search_dirs=[]) + return renderer.render(text, parameters) +