Skip to content
lektor_pythonmarkdown.py 6.29 KiB
Newer Older
# -*- coding: utf-8 -*-
'''
Created on Jun 8, 2018

@author: Patrik Dufresne
'''
import markdown
from markdown.extensions import Extension
from markupsafe import Markup
import traceback
from weakref import ref as weakref
from werkzeug.urls import url_parse
from lektor.context import get_ctx
from lektor.environment import PRIMARY_ALT
from lektor.pluginsystem import Plugin
from lektor.types import Type
from lektor.utils import bool_from_string
SECTION_EXTENSIONS = "extensions"
SECTION_MARKDOWN = "markdown"
Patrik Dufresne's avatar
Patrik Dufresne committed
DEFAULT_EXTENTIONS = {
    "markdown.extensions.extra": 1,
}
def sanitize_link( link):
    """
    Patched function to resolve the url using Lektor.
    """
    if get_ctx() and get_ctx().record is not None:
        url = url_parse(link)
        if not url.scheme:
            return get_ctx().record.url_to(link, base_url=get_ctx().base_url)
    return link
def sanitize_image(link):
    """
    Patched function to resolve the url using Lektor.
    """
    if get_ctx() and get_ctx().record is not None:
        url = url_parse(link)
        if not url.scheme:
            return get_ctx().record.url_to(link, alt=PRIMARY_ALT, base_url=get_ctx().base_url)
    return link


def handleMatch(self, *args, **kwargs):
    """
    Patched function to resolve the href/src using Lektor.
    """
    # Call the original function.
    t = self._handleMatch(*args, **kwargs)  
    if isinstance(t, tuple):
        el = t[0]
    else:
        el = t
    if el is None:
        return t
        
    # Resolv link using lektor
    if el.get('href'):
        el.set('href', sanitize_link(el.get('href')))        
    if el.get('src'):
        el.set('src', sanitize_image(el.get('src')))
    
    return t
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, func):
        """
        Monkey patch the sanitize_url method.
        """
        p._handleMatch = p.handleMatch 
        p.handleMatch = types.MethodType(func, p)
    def extendMarkdown(self, md, md_globals={}):
        self._patch(md.inlinePatterns['link'], handleMatch)
        self._patch(md.inlinePatterns['reference'], handleMatch)
        self._patch(md.inlinePatterns['image_link'], handleMatch)
        self._patch(md.inlinePatterns['image_reference'], handleMatch)

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
            for e, v in (self._section_as_dict(SECTION_EXTENSIONS) or DEFAULT_EXTENTIONS).items()
            if v]
    
    def __init__(self, plugin_config):
        self.plugin_config = plugin_config
        self.options = self._section_as_dict(SECTION_MARKDOWN)
        self.options.update({
            "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.
    try:
        return markdown.markdown(text, **cfg.options)
    except:
        return "pythonmarkdown error: " + traceback.format_exc()


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.__cached_for_ctx = None
        self.__html = None
        self.__meta = None

    def __bool__(self):
        return bool(self.source)

    __nonzero__ = __bool__

    def __render(self):
        # When the markdown instance is attached to a cached object we can
        # end up in the situation where the context changed from the time
        # we were put into the cache to the time where we got referenced
        # by something elsewhere.  In that case we need to re-process our
        # markdown.  For instance this affects relative links.
        if self.__html is None or \
           self.__cached_for_ctx != get_ctx():
            self.__html = pythonmarkdown_to_html(
                self.source, self.__record())
            self.__cached_for_ctx = get_ctx()

    @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):
        self.env.add_type(PythonMarkdownType)