Mercurial > wikked
changeset 464:1dc6a0a74da3
wiki: Improve consistency of absolute/relative links.
- Make links from endpoint pages go to the same endpoint by default.
- Add support for `:` (empty) endpoint to link outside of endpoints.
- Add unit tests.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 06 Oct 2018 19:40:52 -0700 |
parents | fcef742731cf |
children | ccac960348a7 |
files | tests/__init__.py tests/test_resolver.py wikked/formatter.py wikked/resolver.py wikked/utils.py |
diffstat | 5 files changed, 278 insertions(+), 50 deletions(-) [+] |
line wrap: on
line diff
--- a/tests/__init__.py Sat Oct 06 19:37:23 2018 -0700 +++ b/tests/__init__.py Sat Oct 06 19:40:52 2018 -0700 @@ -1,6 +1,6 @@ import os import os.path -import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.parse, urllib.error # noqa import shutil import unittest from wikked.wiki import Wiki @@ -64,15 +64,26 @@ wiki.reset() -def format_link(title, url, missing=False, mod=None): - res = '<a class=\"wiki-link' +def format_link(title, url, missing=False, mod=None, endpoint=None): + res = '<a class="wiki-link' if missing: res += ' missing' + res += '"' + + if endpoint: + url = '%s:%s' % (endpoint, url) url = urllib.parse.quote(url) - res += '\" data-wiki-url=\"' + url + '\"' + res += ' data-wiki-url="%s"' % url + if mod: - res += ' data-wiki-mod=\"' + mod + '\"' - res += ' href="/read' + url + '"' + res += ' data-wiki-mod="%s"' % mod + + if endpoint: + res += ' href="/read/%s"' % url + res += ' data-wiki-endpoint="%s"' % endpoint + else: + res += ' href="/read' + url + '"' + res += '>' + title + '</a>' return res
--- a/tests/test_resolver.py Sat Oct 06 19:37:23 2018 -0700 +++ b/tests/test_resolver.py Sat Oct 06 19:40:52 2018 -0700 @@ -1,3 +1,4 @@ +# flake8: noqa from tests import WikkedTest, format_link, format_include @@ -83,36 +84,210 @@ self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!", base.text) def testDoublePageIncludeWithMeta(self): - return wiki = self._getWikiFromStructure({ 'Base.txt': "The base page.\n{{include: Template 1}}", - 'Wrong.txt': "{{include: Template 2}}", + 'Other.txt': "The other page.\n{{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 = wiki.getPage('/Base') self.assertEqual({ 'foo': ['bar'], - 'category': ['blah', 'yolo'] + 'category': ['blah', 'yolo'], + 'include': ['Template 1', 'Template 2'] }, base.getMeta()) + + other = wiki.getPage('/Other') + self.assertEqual({ + 'category': ['yolo'], + 'include': ['Template 2'] + }, other.getMeta()) + tpl1 = wiki.getPage('/Template 1') self.assertEqual({ 'foo': ['bar'], - '+category': ['blah'], - '+include': ['Template 1'], - '__secret': ['ssh'] + '+category': ['blah', 'yolo'], + '+include': ['Template 2'], + '__secret1': ['ssh'] }, tpl1.getMeta()) + self.assertEqual( - "\n\n%s\n\n" % format_include('/Template 2'), + "\n\n\n", #"\n\n%s\n\n" % format_include('/Template 2'), tpl1.text) - q1 = wiki.getPage('query-1') + q1 = wiki.getPage('/Query 1') self.assertEqual( - "<ul>\n<li>%s</li>\n<li>%s</li>\n</ul>" % (format_link('Base', '/Base'), format_link('Wrong', '/Wrong')), + "\n* %s\n* %s\n\n" % (format_link('Base', '/Base'), format_link('Other', '/Other')), q1.text) - q2 = wiki.getPage('query-2') + q2 = wiki.getPage('/Query 2') self.assertEqual( - "<ul>\n<li>%s</li>\n</ul>" % format_link('Base', '/Base'), + "\n* %s\n\n" % format_link('Base', '/Base'), q2.text) + def testLink1(self): + wiki = self._getWikiFromStructure({ + 'Source.txt': "A link: [[Other]]", + 'Other.txt': "" + }) + + source = wiki.getPage('/Source') + self.assertEqual("A link: %s" % format_link('Other', '/Other'), + source.text) + + def testLink2(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "A link: [[Other]]", + 'Folder/Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual("A link: %s" % format_link('Other', '/Folder/Other'), + source.text) + + def testLink3(self): + wiki = self._getWikiFromStructure({ + 'Source.txt': "[[Folder/Other]]", + 'Folder/Other.txt': "" + }) + + source = wiki.getPage('/Source') + self.assertEqual(format_link('Other', '/Folder/Other'), + source.text) + + def testLink4(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "[[More/Other]]", + 'Folder/More/Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/More/Other'), + source.text) + + def testRelativeLink1(self): + wiki = self._getWikiFromStructure({ + 'Source.txt': "[[./Other]]", + 'Source/Other.txt': "" + }) + + source = wiki.getPage('/Source') + self.assertEqual(format_link('Other', '/Source/Other'), + source.text) + + def testRelativeLink2(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "[[./Other]]", + 'Folder/Source/Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/Source/Other'), + source.text) + + def testRelativeLink3(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "[[../Other]]", + 'Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual(format_link('Other', '/Other'), source.text) + + def testRelativeLink4(self): + wiki = self._getWikiFromStructure({ + 'Folder/More/Source.txt': "[[../Other]]", + 'Folder/Other.txt': "" + }) + + source = wiki.getPage('/Folder/More/Source') + self.assertEqual(format_link('Other', '/Folder/Other'), source.text) + + def testEndpointLink1(self): + wiki = self._getWikiFromStructure({ + 'Source.txt': "[[blah:Other]]", + '_meta/blah/Other.txt': "" + }) + + source = wiki.getPage('/Source') + self.assertEqual(format_link('Other', '/Other', endpoint='blah'), + source.text) + + def testEndpointLink2(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "[[blah:/Other]]", + '_meta/blah/Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual(format_link('Other', '/Other', endpoint='blah'), + source.text) + + def testEndpointLink3(self): + wiki = self._getWikiFromStructure({ + 'Source.txt': "[[blah:/Folder/Other]]", + '_meta/blah/Folder/Other.txt': "" + }) + + source = wiki.getPage('/Source') + self.assertEqual(format_link('Other', '/Folder/Other', endpoint='blah'), + source.text) + + def testEndpointLink4(self): + wiki = self._getWikiFromStructure({ + 'Folder/Source.txt': "[[blah:Other]]", + '_meta/blah/Folder/Other.txt': "" + }) + + source = wiki.getPage('/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/Other', endpoint='blah'), + source.text) + + def testEndpointLink5(self): + wiki = self._getWikiFromStructure({ + '_meta/foo/Folder/Source.txt': "[[blah:Other]]", + '_meta/blah/Folder/Other.txt': "" + }) + + source = wiki.getPage('foo:/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/Other', endpoint='blah'), + source.text) + + def testEndpointLink6(self): + wiki = self._getWikiFromStructure({ + '_meta/blah/Folder/Source.txt': "[[Other]]", + '_meta/blah/Folder/Other.txt': "" + }) + + source = wiki.getPage('blah:/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/Other', endpoint='blah'), + source.text) + + def testEndpointLink7(self): + wiki = self._getWikiFromStructure({ + '_meta/blah/Source.txt': "[[Folder/Other]]", + '_meta/blah/Folder/Other.txt': "" + }) + + source = wiki.getPage('blah:/Source') + self.assertEqual(format_link('Other', '/Folder/Other', endpoint='blah'), + source.text) + + def testEndpointLink8(self): + wiki = self._getWikiFromStructure({ + '_meta/blah/Source.txt': "[[:/Other]]", + 'Other.txt': "" + }) + + source = wiki.getPage('blah:/Source') + self.assertEqual(format_link('Other', '/Other'), source.text) + + def testEndpointLink9(self): + wiki = self._getWikiFromStructure({ + '_meta/blah/Folder/Source.txt': "[[:Other]]", + 'Folder/Other.txt': "" + }) + + source = wiki.getPage('blah:/Folder/Source') + self.assertEqual(format_link('Other', '/Folder/Other'), source.text)
--- a/wikked/formatter.py Sat Oct 06 19:37:23 2018 -0700 +++ b/wikked/formatter.py Sat Oct 06 19:40:52 2018 -0700 @@ -4,7 +4,7 @@ import logging import jinja2 from io import StringIO -from .utils import get_meta_name_and_modifiers, html_escape +from .utils import get_meta_name_and_modifiers, html_escape, split_page_url RE_FILE_FORMAT = re.compile(r'\r\n?', re.MULTILINE) @@ -18,9 +18,9 @@ re.MULTILINE | re.DOTALL) RE_LINK_ENDPOINT = re.compile( - r'\[\[(\w[\w\d]+)\:([^\]]+)\]\]') + r'\[\[(\w[\w\d]+)?\:([^\]]*/)?([^\]]+)\]\]') RE_LINK_ENDPOINT_DISPLAY = re.compile( - r'\[\[([^\|\]]+)\|\s*(\w[\w\d]+)\:([^\]]+)\]\]') + r'\[\[([^\|\]]+)\|\s*(\w[\w\d]+)?\:([^\]]+)\]\]') RE_LINK_DISPLAY = re.compile( r'\[\[([^\|\]]+)\|([^\]]+)\]\]') RE_LINK = re.compile( @@ -34,6 +34,7 @@ """ Base context for formatting pages. """ def __init__(self, url): self.url = url + self.endpoint, self.endpoint_url = split_page_url(url) @property def urldir(self): @@ -127,10 +128,13 @@ # [[endpoint:Something/Blah.ext]] def repl1(m): endpoint = m.group(1) - value = m.group(2).strip() + a, b = m.group(2, 3) + value = b if a is None else (a + b) if endpoint in self.endpoints: - return self.endpoints[endpoint](ctx, endpoint, value, value) - return self._formatEndpointLink(ctx, endpoint, value, value) + return self.endpoints[endpoint]( + ctx, endpoint, b.strip(), value.strip()) + return self._formatEndpointLink( + ctx, endpoint, b.strip(), value.strip()) text = RE_LINK_ENDPOINT.sub(repl1, text) # [[display name|endpoint:Something/Whatever]] @@ -139,8 +143,10 @@ endpoint = m.group(2) value = m.group(3).strip() if endpoint in self.endpoints: - return self.endpoints[endpoint](ctx, endpoint, value, display) - return self._formatEndpointLink(ctx, endpoint, value, display) + return self.endpoints[endpoint]( + ctx, endpoint, display, value) + return self._formatEndpointLink( + ctx, endpoint, display, value) text = RE_LINK_ENDPOINT_DISPLAY.sub(repl2, text) # [[display name|Whatever/PageName]] @@ -153,7 +159,7 @@ def repl4(m): a, b = m.group(1, 2) url = b if a is None else (a + b) - return s._formatWikiLink(ctx, b, url) + return s._formatWikiLink(ctx, b.strip(), url.strip()) text = RE_LINK.sub(repl4, text) return text @@ -208,7 +214,7 @@ return '<div class="wiki-query"%s>%s</div>\n' % ( mod_attr, '|'.join(processed_args)) - def _formatFileLink(self, ctx, endpoint, value, display): + def _formatFileLink(self, ctx, endpoint, display, value): if value.startswith('./'): abs_url = os.path.join('/pagefiles', ctx.url.lstrip('/'), value[2:]) @@ -217,21 +223,40 @@ abs_url = os.path.normpath(abs_url).replace('\\', '/') return abs_url - def _formatImageLink(self, ctx, endpoint, value, display): - abs_url = self._formatFileLink(ctx, endpoint, value, display) + def _formatImageLink(self, ctx, endpoint, display, value): + abs_url = self._formatFileLink(ctx, endpoint, display, value) return ('<img class="wiki-image" src="%s" alt="%s"></img>' % (abs_url, display)) - def _formatEndpointLink(self, ctx, endpoint, value, display): - url = '%s:%s' % (endpoint, value) - ctx.out_links.append(url) - return ('<a class="wiki-link" data-wiki-url="%s" ' - 'data-wiki-endpoint="%s">%s</a>' % (url, endpoint, display)) + def _formatEndpointLink(self, ctx, endpoint, display, local_url): + if True: # endpoint: + endpoint = endpoint or '' + url = '%s:%s' % (endpoint, local_url) + ctx.out_links.append(url) + return ('<a class="wiki-link" data-wiki-url="%s" ' + 'data-wiki-endpoint="%s">%s</a>' % (url, endpoint, + display)) + else: + # Endpoint link was actually: `[[:/Something/Blah]]`, which + # forces going back out of the endpoints. + # Render this like a normal wiki link. + ctx.out_links.append(local_url) + return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % ( + local_url, display) def _formatWikiLink(self, ctx, display, url): - ctx.out_links.append(url) - return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % ( - url, display) + if True: # not ctx.endpoint: + ctx.out_links.append(url) + return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % ( + url, display) + else: + # The link is a relative link from a page that lives in an + # endpoint, so the generated link must be in the same endpoint. + url = '%s:%s' % (ctx.endpoint, url) + ctx.out_links.append(url) + return ('<a class="wiki-link" data-wiki-url="%s" ' + 'data-wiki-endpoint="%s">%s</a>' % (url, ctx.endpoint, + display)) @staticmethod def parseWikiLinks(text):
--- a/wikked/resolver.py Sat Oct 06 19:37:23 2018 -0700 +++ b/wikked/resolver.py Sat Oct 06 19:40:52 2018 -0700 @@ -6,7 +6,7 @@ from wikked.formatter import PageFormatter, FormattingContext from wikked.utils import ( PageNotFoundError, - get_meta_name_and_modifiers, get_absolute_url, + get_meta_name_and_modifiers, get_absolute_url, split_page_url, flatten_single_metas, html_unescape) @@ -21,7 +21,8 @@ r'data-wiki-(?P<name>[a-z]+)="(?P<value>[^"]+)"') re_wiki_link = re.compile( r'<a class="wiki-link(?P<isedit>-edit)?" ' - r'data-wiki-url="(?P<url>[^"]+)"') + r'data-wiki-url="(?P<url>[^"]+)"' + r'( data-wiki-endpoint="(?P<endpoint>[^"]*)")?') re_wiki_include_param = re.compile( r'<div class="wiki-param" ' @@ -88,10 +89,12 @@ return len(self.url_trail) > 1 raise ValueError("Unknown modifier: " + modifier) - def getAbsoluteUrl(self, url, base_url=None, quote=False): + def getAbsoluteUrl(self, url, base_url=None, *, + force_endpoint=None, quote=False): if base_url is None: base_url = self.root_page.url - return get_absolute_url(base_url, url, quote) + return get_absolute_url(base_url, url, + force_endpoint=force_endpoint, quote=quote) class ResolveOutput(object): @@ -114,7 +117,8 @@ if key not in self.meta: self.meta[key] = val else: - self.meta[key] = list(set(self.meta[key] + val)) + existing_metas = set(self.meta[key]) + self.meta[key] += [v for v in val if v not in existing_metas] class PageResolver(object): @@ -225,23 +229,35 @@ # Resolve link states. def repl1(m): raw_url = m.group('url') + endpoint = m.group('endpoint') is_edit = bool(m.group('isedit')) - url = self.ctx.getAbsoluteUrl(raw_url) + url = self.ctx.getAbsoluteUrl(raw_url, force_endpoint=endpoint) validated_url = self.wiki.db.validateUrl(url) if validated_url: url = validated_url + self.output.out_links.append(url) action = 'edit' if is_edit else 'read' quoted_url = urllib.parse.quote(url.encode('utf-8')) + split_url = split_page_url(url) + endpoint_markup = '' + if split_url[0]: + endpoint_markup = ' data-wiki-endpoint="%s"' % split_url[0] if validated_url: + # The DB has confirmed that the target page exists, + # so make a "real" link. actual_url = '/%s/%s' % (action, quoted_url.lstrip('/')) return ('<a class="wiki-link" data-wiki-url="%s" ' - 'href="%s"' % (quoted_url, actual_url)) + 'href="%s"' % (quoted_url, actual_url) + + endpoint_markup) + # The DB doesn't know about the target page, so render + # a link with the "missing" class so it shows up red and all. actual_url = '/%s/%s' % (action, quoted_url.lstrip('/')) return ('<a class="wiki-link missing" data-wiki-url="%s" ' - 'href="%s"' % (quoted_url, actual_url)) + 'href="%s"' % (quoted_url, actual_url) + + endpoint_markup) final_text = re_wiki_link.sub(repl1, final_text) @@ -519,4 +535,3 @@ title = value return ('<a class="wiki-link-edit" data-wiki-url="%s">%s</a>' % (value, title)) -
--- a/wikked/utils.py Sat Oct 06 19:37:23 2018 -0700 +++ b/wikked/utils.py Sat Oct 06 19:40:52 2018 -0700 @@ -9,7 +9,7 @@ re_terminal_path = re.compile(r'[/\\]|(\w\:)') -endpoint_regex = re.compile(r'(\w[\w\d]*)\:(.*)') +endpoint_regex = re.compile(r'(\w[\w\d]+)?\:(.*)') endpoint_prefix_regex = re.compile(r'^(\w[\w\d]+)\:') @@ -55,14 +55,16 @@ return None -def get_absolute_url(base_url, url, quote=False): +def get_absolute_url(base_url, url, *, force_endpoint=None, quote=False): base_endpoint, base_url = split_page_url(base_url) if base_url[0] != '/': raise ValueError("The base URL must be absolute. Got: %s" % base_url) endpoint, url = split_page_url(url) - if not endpoint: - endpoint = base_endpoint + if endpoint is None: + endpoint = force_endpoint + if endpoint is None: + endpoint = base_endpoint if url.startswith('/'): # Absolute page URL.