Mercurial > wikked
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>