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)
+