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.