From 3977c90ae08a243adf8834b8a327fca6c8de597a Mon Sep 17 00:00:00 2001 From: Patrik Dufresne Date: Sat, 16 Jun 2018 08:27:15 -0400 Subject: [PATCH] Implement URL and image link substitution using `url_to()` Alot of modifications was required to make use of `url_to()` during rendering of the markdown. This was required to make sure we have a context when executing the rendering. --- lektor_pythonmarkdown.py | 171 ++++++++++++++---- tests/demo-project/content/contents.lr | 14 ++ .../demo-project/content/sub-page/contents.lr | 3 + tests/test_lektor_pythonmarkdown.py | 16 +- 4 files changed, 162 insertions(+), 42 deletions(-) create mode 100644 tests/demo-project/content/sub-page/contents.lr diff --git a/lektor_pythonmarkdown.py b/lektor_pythonmarkdown.py index 36594c1..93409cc 100644 --- a/lektor_pythonmarkdown.py +++ b/lektor_pythonmarkdown.py @@ -4,9 +4,19 @@ Created on Jun 8, 2018 @author: Patrik Dufresne ''' +import markdown +from markdown.extensions import Extension +from markdown.inlinepatterns import LinkPattern +import types +from weakref import ref as weakref + +from lektor.context import get_ctx, url_to from lektor.pluginsystem import Plugin from lektor.types import Type -import markdown +from lektor.utils import bool_from_string +from markupsafe import Markup +from werkzeug.urls import url_parse + SECTION_EXTENSIONS = "extensions" SECTION_MARKDOWN = "markdown" @@ -15,39 +25,62 @@ DEFAULT_EXTENTIONS = { } -def pythonmarkdown_to_html(text, cfg): - # TODO Call events. - return markdown.markdown(text, **cfg.options) - +def sanitize_url(self, link): + """ + Patched function to resolve the url using Lektor. + """ + try: + url = url_parse(link) + if not url.scheme: + link = url_to("!" + link) + except: + # Do not fail if something went wrong during the url parsing. + pass + return LinkPattern.sanitize_url(self, link) -# Wrapper with an __html__ method prevents Lektor from escaping HTML tags. -class HTML(object): - def __init__(self, html): - self.html = html +class LektorMarkdownExtension(Extension): + """ + This class represent an extension into the python-markdown to implement + some logic in url handling to mimic the current behaviour of default + markdown rendered (mistune). + """ + + def _patch(self, p): + """ + Monkey patch the sanitize_url method. + """ + p.sanitize_url = types.MethodType(sanitize_url, p) - def __html__(self): - return self.html + def extendMarkdown(self, md, md_globals): + self._patch(md.inlinePatterns['link']) + self._patch(md.inlinePatterns['image_link']) + self._patch(md.inlinePatterns['image_reference']) + self._patch(md.inlinePatterns['reference']) - -def _value(v): - "This function tries to convert the configuration value to a sane type." - if v.lower() in ['true', 'false']: - return v.lower() == 'true' - try: - return int(v) - except: - return v - class PythonMarkdownConfig(object): """ Define configuration of python-markdown. """ def _section_as_dict(self, name): + + def _value(v): + "This function tries to convert the configuration value to a sane type." + w = bool_from_string(v) + if w is not None: + return w + try: + return int(v) + except: + return v + return {k: _value(v) for k, v in self.plugin_config.section_as_dict(name).items()} + def _builtin_extensions(self): + return [LektorMarkdownExtension()] + def _extensions(self): return [ e @@ -58,30 +91,92 @@ class PythonMarkdownConfig(object): self.plugin_config = plugin_config self.options = self._section_as_dict(SECTION_MARKDOWN) self.options.update({ - "extensions": self._extensions(), + "extensions": self._extensions() + self._builtin_extensions(), "extension_configs": {e: self._section_as_dict(e) for e in self._extensions()}, }) +def pythonmarkdown_to_html(text, record=None): + """ + Convert python-markdown into html. + """ + ctx = get_ctx() + if ctx is None: + raise RuntimeError('Context is required for python-markdown rendering') + + env = get_ctx().env + plugin = env.plugins.get('pythonmarkdown', None) + if not plugin: + raise RuntimeError('PythonMarkdownPLugin is required for python-markdown rendering') + cfg = PythonMarkdownConfig(plugin.get_config()) + # TODO May need to emit event to let other plugin hook into this one. + return markdown.markdown(text, **cfg.options) + + +class PythonMarkdown(object): + + def __init__(self, source, record=None): + self.source = source + self.__record = weakref(record) if record is not None else lambda: None + self.__html = None + self.__meta = None + + def __bool__(self): + return bool(self.source) + + __nonzero__ = __bool__ + + def __render(self): + if self.__html is None: + self.__html = pythonmarkdown_to_html(self.source, self.__record()) + self.__meta = None + + @property + def meta(self): + self.__render() + return self.__meta + + @property + def html(self): + self.__render() + return Markup(self.__html) + + def __getitem__(self, name): + return self.meta[name] + + def __unicode__(self): + self.__render() + return self.__html + + def __html__(self): + self.__render() + return Markup(self.__html) + + +class PythonMarkdownDescriptor(object): + + def __init__(self, source): + self.source = source + + def __get__(self, obj, type=None): + if obj is None: + return self + return PythonMarkdown(self.source, record=obj) + + +class PythonMarkdownType(Type): + widget = 'multiline-text' + + def value_from_raw(self, raw): + """ + Called to convert the raw value (markdown) into html. + """ + return PythonMarkdownDescriptor(raw.value or u'') + + class PythonMarkdownPlugin(Plugin): name = u'pythonmarkdown' description = u'Add pythonmarkdownn field type to Lektor to make use of python-markdown as a renderer.' def on_setup_env(self, **extra): - plugin_config = self.get_config() - - # We declare this type as an internal class inside the plugin to get - # access to the get_config() function from the Type. - # - # The name of the class is used a key for the fields type. - class PythonMarkdownType(Type): - widget = 'multiline-text' - - def value_from_raw(self, raw): - """ - Called to convert the raw value (markdown) into html. - """ - cfg = PythonMarkdownConfig(plugin_config) - return HTML(pythonmarkdown_to_html(raw.value or u'', cfg)) - self.env.add_type(PythonMarkdownType) diff --git a/tests/demo-project/content/contents.lr b/tests/demo-project/content/contents.lr index 570ab7b..384d5d8 100644 --- a/tests/demo-project/content/contents.lr +++ b/tests/demo-project/content/contents.lr @@ -7,3 +7,17 @@ body: ``` code here ``` + +## Check url & image substitution + +[Link to Sub Page](sub-page) + +![alttxt](sub-page) + +## Check references + +I get 10 times more traffic from [Sub Page] [1] than from +[Yahoo] [2]. + + [1]: sub-page/ "Sub Page" + [2]: http://search.yahoo.com/ "Yahoo Search" diff --git a/tests/demo-project/content/sub-page/contents.lr b/tests/demo-project/content/sub-page/contents.lr new file mode 100644 index 0000000..51ec7db --- /dev/null +++ b/tests/demo-project/content/sub-page/contents.lr @@ -0,0 +1,3 @@ +body: + +Nothing special \ No newline at end of file diff --git a/tests/test_lektor_pythonmarkdown.py b/tests/test_lektor_pythonmarkdown.py index ff9443e..4b7a3f2 100644 --- a/tests/test_lektor_pythonmarkdown.py +++ b/tests/test_lektor_pythonmarkdown.py @@ -5,15 +5,16 @@ Created on Jun 8, 2018 @author: Patrik Dufresne ''' -from lektor.builder import Builder -from lektor.db import Database -from lektor.environment import Environment -from lektor.project import Project import os import shutil import tempfile import unittest +from lektor.builder import Builder +from lektor.db import Database +from lektor.environment import Environment +from lektor.project import Project + class TestLektorPythonMarkdown(unittest.TestCase): @@ -40,6 +41,13 @@ class TestLektorPythonMarkdown(unittest.TestCase): assert '

Header 2

' in html # The output changes depending on the version of python-markdown uses. assert '
code here
' in html + # Check url & image substitution + assert 'Link to Sub Page' in html + assert 'alttxt' in html + # Check references + assert 'Sub Page' in html + assert 'Yahoo' in html + if __name__ == "__main__": -- GitLab