changeset 84:ca57fef14d04

Formatter/resolver changes: - Formatting is done after resolving. - Resolving includes passing text through the template engine. - Using Jinja2 for templating now. - Added unit tests.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 03 Apr 2013 23:30:23 -0700
parents 65f83a9b42f1
children ba440e5e4059
files tests/test_page.py tests/test_resolver.py wikked/formatter.py wikked/page.py wikked/resolver.py wikked/templates/circular_include_error.html
diffstat 6 files changed, 163 insertions(+), 66 deletions(-) [+]
line wrap: on
line diff
--- a/tests/test_page.py	Sun Mar 24 22:06:50 2013 -0700
+++ b/tests/test_page.py	Wed Apr 03 23:30:23 2013 -0700
@@ -9,6 +9,9 @@
             })
         page = Page(self.wiki, 'foo')
         self.assertEqual('foo', page.url)
+        self.assertEqual('foo.txt', page.path)
+        self.assertEqual('foo', page.filename)
+        self.assertEqual('txt', page.extension)
         self.assertEqual('A test page.', page.raw_text)
         self.assertEqual('A test page.', page._getFormattedText())
         self.assertEqual('foo', page.title)
@@ -25,7 +28,7 @@
         self.assertEqual("A page with simple meta.\n{{bar: baz}}\n{{is_test: }}", page.raw_text)
         self.assertEqual('A page with simple meta.\n\n', page._getFormattedText())
         self.assertEqual('foo', page.title)
-        self.assertEqual('A page with simple meta.\n\n', page.text)
+        self.assertEqual('A page with simple meta.\n', page.text)
         self.assertEqual({'bar': ['baz'], 'is_test': True}, page._getLocalMeta())
         self.assertEqual([], page._getLocalLinks())
 
@@ -38,7 +41,7 @@
         self.assertEqual("A page with a custom title.\n{{title: TEST-TITLE}}", page.raw_text)
         self.assertEqual('A page with a custom title.\n', page._getFormattedText())
         self.assertEqual('TEST-TITLE', page.title)
-        self.assertEqual('A page with a custom title.\n', page.text)
+        self.assertEqual('A page with a custom title.', page.text)
         self.assertEqual({'title': ['TEST-TITLE']}, page._getLocalMeta())
         self.assertEqual([], page._getLocalLinks())
 
@@ -83,3 +86,12 @@
         foo = Page(self.wiki, 'foo')
         self.assertEqual("URL: /files/blah/boo/image.png", foo._getFormattedText())
 
+    def testUrlTemplateFunctions(self):
+        self.wiki =self._getWikiFromStructure({
+            'foo.txt': "Here is {{read_url(__page.url, 'FOO')}}!"
+            })
+        foo = Page(self.wiki, 'foo')
+        self.assertEqual(
+            'Here is <a class="wiki-link" data-wiki-url="foo">FOO</a>!',
+            foo.text
+            )
--- a/tests/test_resolver.py	Sun Mar 24 22:06:50 2013 -0700
+++ b/tests/test_resolver.py	Wed Apr 03 23:30:23 2013 -0700
@@ -13,7 +13,7 @@
         self.assertEqual(
                 "A test page.\n%s" % format_include('trans-desc'),
                 foo._getFormattedText())
-        self.assertEqual("A test page.\nBLAH\n\n", foo.text)
+        self.assertEqual("A test page.\nBLAH", foo.text)
 
     def testPageIncludeWithMeta(self):
         self.wiki = self._getWikiFromStructure({
@@ -27,7 +27,7 @@
                 "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),
+                "A test page.\nBLAH: %s\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)
@@ -41,18 +41,29 @@
         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)
+        self.assertEqual("A test page.\nHello Dave, would you like a drink?", 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}}."
+            'Greeting.txt': "Hello {{__args[0]}}, {{__args[1]}} and {{__args[2]}}."
             })
         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)
+        self.assertEqual("A test page.\nHello Dave, Roger and Tom.", foo.text)
+
+    def testIncludeWithPageReferenceTemplating(self):
+        self.wiki =self._getWikiFromStructure({
+            'SelfRef.txt': "Here is {{read_url(__page.url, __page.title)}}!",
+            'Foo.txt': "Hello here.\n{{include: selfref}}\n"
+            })
+        foo = Page(self.wiki, 'foo')
+        self.assertEqual(
+            'Hello here.\nHere is <a class="wiki-link" data-wiki-url="foo">Foo</a>!',
+            foo.text
+            )
 
     def testGivenOnlyInclude(self):
         self.wiki = self._getWikiFromStructure({
@@ -64,9 +75,9 @@
         self.assertEqual(
                 "TEMPLATE!\n%s" % format_include('template-2', mod='+'),
                 tpl1._getFormattedText())
-        self.assertEqual("TEMPLATE!\n\n", tpl1.text)
+        self.assertEqual("TEMPLATE!\n", tpl1.text)
         base = Page(self.wiki, 'base')
-        self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!\n\n", base.text)
+        self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!", base.text)
 
     def testDoublePageIncludeWithMeta(self):
         return
@@ -80,7 +91,7 @@
             })
         base = Page(self.wiki, 'base')
         self.assertEqual({
-            'foo': ['bar'], 
+            'foo': ['bar'],
             'category': ['blah', 'yolo']
             }, base.meta)
         tpl1 = Page(self.wiki, 'template-1')
--- a/wikked/formatter.py	Sun Mar 24 22:06:50 2013 -0700
+++ b/wikked/formatter.py	Wed Apr 03 23:30:23 2013 -0700
@@ -4,13 +4,6 @@
 from metautils import get_meta_name_and_modifiers
 
 
-class FormatterNotFound(Exception):
-    """ An exception raised when not formatter is found for the
-        current page.
-    """
-    pass
-
-
 class BaseContext(object):
     """ Base context for formatting pages. """
     def __init__(self, url, slugify=None):
@@ -43,9 +36,8 @@
 
 class FormattingContext(BaseContext):
     """ Context for formatting pages. """
-    def __init__(self, url, ext, slugify):
+    def __init__(self, url, slugify):
         BaseContext.__init__(self, url, slugify)
-        self.ext = ext
         self.out_links = []
         self.meta = {}
 
@@ -66,26 +58,14 @@
                 }
 
     def formatText(self, ctx, text):
-        text = self._preProcessWikiSyntax(ctx, text)
-        formatter = self._getFormatter(ctx.ext)
-        text = formatter(text)
-        text = self._postProcessWikiSyntax(ctx, text)
-        return formatter(text)
+        text = self._processWikiSyntax(ctx, text)
+        return text
 
-    def _getFormatter(self, extension):
-        for k, v in self.wiki.formatters.iteritems():
-            if extension in v:
-                return k
-        raise FormatterNotFound("No formatter mapped to file extension: " + extension)
-
-    def _preProcessWikiSyntax(self, ctx, text):
+    def _processWikiSyntax(self, ctx, text):
         text = self._processWikiMeta(ctx, text)
         text = self._processWikiLinks(ctx, text)
         return text
 
-    def _postProcessWikiSyntax(self, ctx, text):
-        return text
-
     def _processWikiMeta(self, ctx, text):
         def repl(m):
             meta_name = str(m.group('name')).lower()
--- a/wikked/page.py	Sun Mar 24 22:06:50 2013 -0700
+++ b/wikked/page.py	Wed Apr 03 23:30:23 2013 -0700
@@ -3,7 +3,6 @@
 import re
 import datetime
 import unicodedata
-import pystache
 from formatter import PageFormatter, FormattingContext
 from resolver import PageResolver, CircularIncludeError
 
@@ -36,6 +35,16 @@
         return self._data.path
 
     @property
+    def extension(self):
+        self._ensureData()
+        return self._data.extension
+
+    @property
+    def filename(self):
+        self._ensureData()
+        return self._data.filename
+
+    @property
     def title(self):
         self._ensureData()
         return self._data.title
@@ -111,12 +120,14 @@
         page_info = self.wiki.fs.getPage(self.url)
         data.path = page_info.path
         data.raw_text = page_info.content
+        split = os.path.splitext(data.path)
+        data.filename = split[0]
+        data.extension = split[1].lstrip('.')
 
         # Format the page and get the meta properties.
         filename = os.path.basename(data.path)
         filename_split = os.path.splitext(filename)
-        extension = filename_split[1].lstrip('.')
-        ctx = FormattingContext(self.url, extension, slugify=Page.title_to_url)
+        ctx = FormattingContext(self.url, slugify=Page.title_to_url)
         f = PageFormatter(self.wiki)
         data.formatted_text = f.formatText(ctx, data.raw_text)
         data.local_meta = ctx.meta
@@ -147,8 +158,9 @@
                     'circular_include_error.html'
                     )
             with open(template_path, 'r') as f:
-                template = pystache.compile(f.read())
-            self._data.text = template({
+                env = jinja2.Environment()
+                template = env.from_string(f.read())
+            self._data.text = template.render({
                     'message': str(cie),
                     'url_trail': cie.url_trail
                     })
@@ -197,6 +209,9 @@
                 return None
         data = PageData()
         data.path = db_page.path
+        split = os.path.splitext(data.path)
+        data.filename = split[0]
+        data.extension = split[1]
         data.title = db_page.title
         data.raw_text = db_page.raw_text
         data.formatted_text = db_page.formatted_text
--- a/wikked/resolver.py	Sun Mar 24 22:06:50 2013 -0700
+++ b/wikked/resolver.py	Wed Apr 03 23:30:23 2013 -0700
@@ -1,8 +1,15 @@
 import re
-import pystache
+import jinja2
 from metautils import get_meta_name_and_modifiers
 
 
+class FormatterNotFound(Exception):
+    """ An exception raised when not formatter is found for the
+        current page.
+    """
+    pass
+
+
 class CircularIncludeError(Exception):
     """ An exception raised when a circular include is found
         while rendering a page.
@@ -14,10 +21,11 @@
 
 class ResolveContext(object):
     """ The context for resolving page queries. """
-    def __init__(self, root_url=None):
+    def __init__(self, root_page=None):
+        self.root_page = root_page
         self.url_trail = set()
-        if root_url:
-            self.url_trail.add(root_url)
+        if root_page:
+            self.url_trail.add(root_page.url)
 
     def shouldRunMeta(self, modifier):
         if modifier is None:
@@ -70,14 +78,24 @@
         self.page = page
         self.ctx = ctx
         self.output = None
+        self.env = None
 
     @property
     def wiki(self):
         return self.page.wiki
 
+    @property
+    def is_root(self):
+        return self.page == self.ctx.root_page
+
     def run(self):
+        # Create the context object.
         if not self.ctx:
-            self.ctx = ResolveContext(self.page.url)
+            self.ctx = ResolveContext(self.page)
+
+        # Create the output object, so it can be referenced and merged
+        # with child outputs (from included pages).
+        self.output = ResolveOutput(self.page)
 
         # Resolve link states.
         def repl1(m):
@@ -85,8 +103,8 @@
             if self.wiki.pageExists(url):
                 return str(m.group())
             return '<a class="wiki-link missing" data-wiki-url="%s">' % url
-        
-        formatted_text = re.sub(
+
+        final_text = re.sub(
                 r'<a class="wiki-link" data-wiki-url="(?P<url>[^"]+)">',
                 repl1,
                 self.page._getFormattedText())
@@ -98,7 +116,7 @@
             meta_opts = {}
             if m.group('opts'):
                 for c in re.finditer(
-                        r'data-wiki-(?P<name>[a-z]+)="(?P<value>[^"]+)"', 
+                        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'))
@@ -110,15 +128,29 @@
                 return self._runInclude(meta_opts, meta_value)
             return ''
 
-        self.output = ResolveOutput(self.page)
-        self.output.text = re.sub(
+        final_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,
+                final_text,
                 flags=re.MULTILINE)
 
+        # Run text through templating and formatting if this
+        # is the root page.
+        if self.is_root:
+            parameters = {
+                    '__page': {
+                        'url': self.page.url,
+                        'title': self.page.title
+                        }
+                    }
+            final_text = self._renderTemplate(final_text, parameters, error_url=self.page.url)
+            formatter = self._getFormatter(self.page.extension)
+            final_text = formatter(final_text)
+
+        # Assign the final text and return.
+        self.output.text = final_text
         return self.output
 
     def _runInclude(self, opts, args):
@@ -133,15 +165,26 @@
             raise CircularIncludeError("Circular include detected at: %s" % include_url, self.ctx.url_trail)
 
         # Parse the templating parameters.
-        parameters = None
+        parameters = {
+                '__page': {
+                    'url': self.ctx.root_page.url,
+                    'title': self.ctx.root_page.title
+                    },
+                '__args': []
+                }
         if args:
-            parameters = {}
+            # For each parameter, we render templated expressions in case
+            # they depend on parent paremeters passed to the call.
+            # We do not, however, run them through the formatting -- this
+            # will be done in one pass when everything is gathered on the
+            # root page.
             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()
+                value = self._renderTemplate(value, parameters, error_url=self.page.url)
                 parameters[key] = value
-                parameters[str(i + 1)] = value
+                parameters['__args'].append(value)
 
         # Re-run the resolver on the included page to get its final
         # formatted text.
@@ -151,10 +194,9 @@
         child_output = child.run()
         self.output.add(child_output)
 
-        # Run some simple templating if we need to.
+        # Run the templating.
         text = child_output.text
-        if parameters:
-            text = self._renderTemplate(text, parameters)
+        text = self._renderTemplate(text, parameters, error_url=include_url)
 
         return text
 
@@ -198,17 +240,22 @@
                     'title': p.title
                     }
             tokens.update(p._getLocalMeta())
-            text += self._renderTemplate(
-                    self._valueOrPageText(parameters['__item']),
-                    tokens)
+            item_url, item_text = self._valueOrPageText(parameters['__item'], with_url=True)
+            text += self._renderTemplate(item_text, tokens, error_url=item_url or self.page.url)
         text += self._valueOrPageText(parameters['__footer'])
 
         return text
 
-    def _valueOrPageText(self, value):
+    def _valueOrPageText(self, value, with_url=False):
         if re.match(r'^\[\[.*\]\]$', value):
             page = self.wiki.getPage(value[2:-2])
+            if with_url:
+                return (page.url, page.text)
             return page.text
+
+        value = self._renderTemplate(value, None)
+        if with_url:
+            return (None, value)
         return value
 
     def _isPageMatch(self, page, name, value, level=0):
@@ -257,7 +304,39 @@
 
         return False
 
-    def _renderTemplate(self, text, parameters):
-        renderer = pystache.Renderer(search_dirs=[])
-        return renderer.render(text, parameters)
+    def _getFormatter(self, extension):
+        known_exts = []
+        for k, v in self.page.wiki.formatters.iteritems():
+            if extension in v:
+                return k
+            known_exts += v
+        raise FormatterNotFound(
+            "No formatter mapped to file extension '%s' (known extensions: %s)" %
+            (extension, known_exts))
+
+    def _renderTemplate(self, text, parameters, error_url=None):
+        env = self._getJinjaEnvironment()
+        try:
+            template = env.from_string(text)
+            return template.render(parameters)
+        except jinja2.TemplateSyntaxError as tse:
+            raise Exception("Error in '%s': %s\n%s" % (error_url or 'Unknown URL', tse, text))
 
+    def _getJinjaEnvironment(self):
+        if self.env is None:
+            self.env = jinja2.Environment()
+            self.env.globals['read_url'] = generate_read_url
+            self.env.globals['edit_url'] = generate_edit_url
+        return self.env
+
+
+def generate_read_url(value, title=None):
+    if title is None:
+        title = value
+    return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % (value, title)
+
+def generate_edit_url(value, title=None):
+    if title is None:
+        title = value
+    return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % (value, title)
+
--- a/wikked/templates/circular_include_error.html	Sun Mar 24 22:06:50 2013 -0700
+++ b/wikked/templates/circular_include_error.html	Wed Apr 03 23:30:23 2013 -0700
@@ -2,8 +2,8 @@
     <p>{{message}}</p>
     <p>Here are the URLs we followed:</p>
     <ul>
-        {{#url_trail}}
-        <li>{{.}}</li>
-        {{/url_trail}}
+        {% for url in url_trail %}
+        <li>{{url}}</li>
+        {% endfor %}
     </ul>
 </div>