From: Michael Tremer Date: Thu, 30 May 2019 13:15:15 +0000 (+0100) Subject: wiki: Add preview for editing pages X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=2901b734e6f1c348ab10bdddf86d50f47cb29d0b;p=ipfire.org.git wiki: Add preview for editing pages Signed-off-by: Michael Tremer --- diff --git a/src/backend/wiki.py b/src/backend/wiki.py index 51aa2b26..e3275c2a 100644 --- a/src/backend/wiki.py +++ b/src/backend/wiki.py @@ -213,20 +213,13 @@ class Wiki(misc.Object): if file and file.is_image(): return file + def render(self, path, text): + r = WikiRenderer(self.backend, path) -class Page(misc.Object): - # Wiki links - wiki_link = re.compile(r"\[\[([\w\d\/\-\.]+)(?:\|(.+?))?\]\]") - - # External links - external_link = re.compile(r"\[\[((?:ftp|git|https?|rsync|sftp|ssh|webcal)\:\/\/.+?)(?:\|(.+?))?\]\]") - - # Interwiki links e.g. [[wp>IPFire]] - interwiki_link = re.compile(r"\[\[(\w+)>(.+?)(?:\|(.+?))?\]\]") + return r.render(text) - # Mail link - email_link = re.compile(r"\[\[([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)(?:\|(.+?))?\]\]") +class Page(misc.Object): def init(self, id, data=None): self.id = id self.data = data @@ -296,127 +289,13 @@ class Page(misc.Object): if self.data.author_uid: return self.backend.accounts.get_by_uid(self.data.author_uid) - def _render_wiki_link(self, m): - path, alias = m.groups() - - # Allow relative links - if not path.startswith("/"): - path = os.path.join(self.page, path) - - # Normalise links - path = os.path.normpath(path) - - return """%s""" % ( - path, - alias or self.backend.wiki.get_page_title(path), - ) - - def _render_external_link(self, m): - url, alias = m.groups() - - return """%s""" % (url, alias or url) - - def _render_interwiki_link(self, m): - wiki = m.group(1) - if not wiki: - return - - # Retrieve URL - try: - url, repl, icon = INTERWIKIS[wiki] - except KeyError: - logging.warning("Invalid interwiki: %s" % wiki) - return - - # Name of the page - name = m.group(2) - - # Expand URL - url = url % { - "name" : name, - "url" : urllib.parse.quote(name), - } - - # Get alias (if present) - alias = m.group(3) - - if not alias and repl: - alias = repl % name - - # Put everything together - s = [] - - if icon: - s.append("" % icon) - - s.append("""%s""" % (url, alias or name)) - - return " ".join(s) - - def _render_email_link(self, m): - address, alias = m.groups() - - return """%s""" \ - % (address, alias or address) - - def _render(self, text): - logging.debug("Rendering %s" % self) - - # Link images - replacements = [] - for match in re.finditer(r"!\[(.*?)\]\((.*?)\)", text): - alt_text, url = match.groups() - - # Skip any absolute and external URLs - if url.startswith("/") or url.startswith("https://") or url.startswith("http://"): - continue - - # Try to split query string - url, delimiter, qs = url.partition("?") - - # Parse query arguments - args = urllib.parse.parse_qs(qs) - - # Find image - file = self.backend.wiki.find_image(self.page, url) - if not file: - continue - - # Scale down the image if not already done - if not "s" in args: - args["s"] = "768" - - # Format URL - url = "%s?%s" % (file.url, urllib.parse.urlencode(args)) - - replacements.append((match.span(), file, alt_text, url)) - - # Apply all replacements - for (start, end), file, alt_text, url in reversed(replacements): - text = text[:start] + "[![%s](%s)](%s?action=detail)" % (alt_text, url, file.url) + text[end:] - - # Handle wiki links - text = self.wiki_link.sub(self._render_wiki_link, text) - - # Handle interwiki links - text = self.interwiki_link.sub(self._render_interwiki_link, text) - - # Handle external links - text = self.external_link.sub(self._render_external_link, text) - - # Handle email links - text = self.email_link.sub(self._render_email_link, text) - - # Borrow this from the blog - return self.backend.blog._render_text(text, lang="markdown") - @property def markdown(self): return self.data.markdown or "" @property def html(self): - return self._render(self.markdown) + return self.backend.wiki.render(self.page, self.markdown) @property def timestamp(self): @@ -600,3 +479,134 @@ class File(misc.Object): self.memcache.set(cache_key, thumbnail) return thumbnail + + +class WikiRenderer(misc.Object): + # Wiki links + wiki_link = re.compile(r"\[\[([\w\d\/\-\.]+)(?:\|(.+?))?\]\]") + + # External links + external_link = re.compile(r"\[\[((?:ftp|git|https?|rsync|sftp|ssh|webcal)\:\/\/.+?)(?:\|(.+?))?\]\]") + + # Interwiki links e.g. [[wp>IPFire]] + interwiki_link = re.compile(r"\[\[(\w+)>(.+?)(?:\|(.+?))?\]\]") + + # Mail link + email_link = re.compile(r"\[\[([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)(?:\|(.+?))?\]\]") + + def init(self, path): + self.path = path + + def _render_wiki_link(self, m): + path, alias = m.groups() + + # Allow relative links + if not path.startswith("/"): + path = os.path.join(self.path, path) + + # Normalise links + path = os.path.normpath(path) + + return """%s""" % ( + path, + alias or self.backend.wiki.get_page_title(path), + ) + + def _render_external_link(self, m): + url, alias = m.groups() + + return """%s""" % (url, alias or url) + + def _render_interwiki_link(self, m): + wiki = m.group(1) + if not wiki: + return + + # Retrieve URL + try: + url, repl, icon = INTERWIKIS[wiki] + except KeyError: + logging.warning("Invalid interwiki: %s" % wiki) + return + + # Name of the page + name = m.group(2) + + # Expand URL + url = url % { + "name" : name, + "url" : urllib.parse.quote(name), + } + + # Get alias (if present) + alias = m.group(3) + + if not alias and repl: + alias = repl % name + + # Put everything together + s = [] + + if icon: + s.append("" % icon) + + s.append("""%s""" % (url, alias or name)) + + return " ".join(s) + + def _render_email_link(self, m): + address, alias = m.groups() + + return """%s""" \ + % (address, alias or address) + + def render(self, text): + logging.debug("Rendering %s" % self.path) + + # Link images + replacements = [] + for match in re.finditer(r"!\[(.*?)\]\((.*?)\)", text): + alt_text, url = match.groups() + + # Skip any absolute and external URLs + if url.startswith("/") or url.startswith("https://") or url.startswith("http://"): + continue + + # Try to split query string + url, delimiter, qs = url.partition("?") + + # Parse query arguments + args = urllib.parse.parse_qs(qs) + + # Find image + file = self.backend.wiki.find_image(self.path, url) + if not file: + continue + + # Scale down the image if not already done + if not "s" in args: + args["s"] = "768" + + # Format URL + url = "%s?%s" % (file.url, urllib.parse.urlencode(args)) + + replacements.append((match.span(), file, alt_text, url)) + + # Apply all replacements + for (start, end), file, alt_text, url in reversed(replacements): + text = text[:start] + "[![%s](%s)](%s?action=detail)" % (alt_text, url, file.url) + text[end:] + + # Handle wiki links + text = self.wiki_link.sub(self._render_wiki_link, text) + + # Handle interwiki links + text = self.interwiki_link.sub(self._render_interwiki_link, text) + + # Handle external links + text = self.external_link.sub(self._render_external_link, text) + + # Handle email links + text = self.email_link.sub(self._render_email_link, text) + + # Borrow this from the blog + return self.backend.blog._render_text(text, lang="markdown") diff --git a/src/scss/style.scss b/src/scss/style.scss index e183595b..70097153 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -30,6 +30,7 @@ @import "../bootstrap/scss/list-group"; @import "../bootstrap/scss/close"; @import "../bootstrap/scss/modal"; +@import "../bootstrap/scss/spinners"; @import "../bootstrap/scss/utilities"; @import "../bootstrap/scss/print"; @@ -315,6 +316,29 @@ section { } } +#preview { + // Hide the spinner by default + #spinner { + display: none; + } + + #preview-content { + @include transition(opacity .5s linear); + } + + &.updating { + // Show the spinner during updates + #spinner { + display: block; + } + + // While updating, we face out the content + #preview-content { + opacity: 0.5; + } + } +} + hr.divider { border-color: rgba($dark, .15); margin-top: 2rem; diff --git a/src/templates/wiki/edit.html b/src/templates/wiki/edit.html index c5bec0ea..8933b400 100644 --- a/src/templates/wiki/edit.html +++ b/src/templates/wiki/edit.html @@ -11,7 +11,7 @@ {% end block %} {% block main %} -
+

{% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %} @@ -21,7 +21,7 @@ {% raw xsrf_form_html() %}
-
@@ -51,4 +51,59 @@

+ +
+
+

{{ _("Preview") }}

+ +
+ +
+
+
+ {{ _("Loading...") }} +
+
+
+
+{% end block %} + +{% block javascript %} + {% end block %} diff --git a/src/web/__init__.py b/src/web/__init__.py index ea797952..af11c98f 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -290,6 +290,7 @@ class Application(tornado.web.Application): # Actions (r"([A-Za-z0-9\-_\/]+)?/_edit", wiki.ActionEditHandler), + (r"([A-Za-z0-9\-_\/]+)?/_render", wiki.ActionRenderHandler), (r"([A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", wiki.ActionWatchHandler), (r"/actions/upload", wiki.ActionUploadHandler), diff --git a/src/web/wiki.py b/src/web/wiki.py index 5a37ea22..b4c01e39 100644 --- a/src/web/wiki.py +++ b/src/web/wiki.py @@ -105,6 +105,20 @@ class ActionWatchHandler(auth.CacheMixin, base.BaseHandler): self.redirect(page.url) +class ActionRenderHandler(auth.CacheMixin, base.BaseHandler): + def check_xsrf_cookie(self): + pass # disabled + + @tornado.web.authenticated + def post(self, path): + content = self.get_argument("content") + + # Render the content + html = self.backend.wiki.render(path, content) + + self.finish(html) + + class FilesHandler(auth.CacheMixin, base.BaseHandler): @tornado.web.authenticated def get(self, path):