From fcd36bf4bc8addbb26392b390ca9efcc9bcc4d38 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 23 Oct 2017 11:09:48 +0200 Subject: [PATCH] Backport Pelican theme and plugins to work with stock 3.7.1. I hate what I just did, but it was inevitable. --- doc/pelican.rst | 13 ++- doc/plugins/htmlsanity.rst | 6 -- pelican-plugins/m/htmlsanity.py | 161 +++++++++++++++++++++++++++++++- site/pelicanconf.py | 16 +++- site/publishconf.py | 2 +- 5 files changed, 177 insertions(+), 21 deletions(-) diff --git a/doc/pelican.rst b/doc/pelican.rst index 4fc770e5..8fa7ffcc 100644 --- a/doc/pelican.rst +++ b/doc/pelican.rst @@ -32,7 +32,7 @@ Pelican Python and unlike most other static site generators, it uses `reStructuredText `_ instead of Markdown for authoring content. ``m.css`` provides a theme for it, together -with a set of useful `plugins <{filename}/plugins.rst>`_. +with a set of useful plugins. .. note-warning:: @@ -44,15 +44,14 @@ with a set of useful `plugins <{filename}/plugins.rst>`_. `Quick start`_ ============== -Note that currently most of the functionality provided by ``m.css`` requires -patches that aren't integrated into any released version yet, so it's -recommended to install a patched version using Python's ``pip`` instead of -using the stable 3.7.1 release. Note that in order to use ``m.css`` plugins -later, you want to install the Python 3 version: +Install Pelican either via ``pip`` or using your system package manager. Note +that in order to use ``m.css`` `plugins <{filename}/plugins.rst>`_ later, you +may want to install the Python 3 version. .. code:: sh - pip install git+https://github.com/mosra/pelican.git@mosra-master + # You may need sudo here + pip install pelican Once you have Pelican installed, create a directory for your website and bootstrap it: diff --git a/doc/plugins/htmlsanity.rst b/doc/plugins/htmlsanity.rst index 8ac4ed1a..79314ef1 100644 --- a/doc/plugins/htmlsanity.rst +++ b/doc/plugins/htmlsanity.rst @@ -52,12 +52,6 @@ including the ``m/`` directory into one of your :py:`PLUGIN_PATHS` and add HTMLSANITY_SMART_QUOTES = True HTMLSANITY_HYPHENATION = True -.. note-warning:: - - Note that you need Pelican with :gh:`getpelican/pelican@7336de45cbb5f60e934b65f823d0583b48a6c96b` - applied for this to work properly. It's scheduled to be part of - yet-to-be-released version 3.8. - `What it does`_ =============== diff --git a/pelican-plugins/m/htmlsanity.py b/pelican-plugins/m/htmlsanity.py index 4e55a02a..b6bf9426 100644 --- a/pelican-plugins/m/htmlsanity.py +++ b/pelican-plugins/m/htmlsanity.py @@ -25,6 +25,9 @@ import os.path import re +import six +from six.moves.urllib.parse import urlparse, urlunparse + from docutils.writers.html5_polyglot import HTMLTranslator from docutils.transforms import Transform import docutils @@ -33,6 +36,11 @@ from docutils.utils import smartquotes import pelican.signals from pelican.readers import RstReader +from pelican.contents import Content, Author, Category, Tag, Static + +import logging + +logger = logging.getLogger(__name__) try: import pyphen @@ -512,6 +520,61 @@ class SaneRstReader(RstReader): writer_class = SaneHtmlWriter field_body_translator_class = _SaneFieldBodyTranslator +# Implementation of SaneRstReader adapted from +# https://github.com/getpelican/pelican/blob/7336de45cbb5f60e934b65f823d0583b48a6c96b/pelican/readers.py#L206 +# for compatibility with stock Pelican 3.7.1 that doesn't have writer_class or +# field_body_translator_class fields, so we override _parse_metadata and +# _get_publisher directly. +# TODO: remove when 3.8 with https://github.com/getpelican/pelican/pull/2163 +# is released +class SaneRstReaderPelican371(RstReader): + def _parse_metadata(self, document): + """Return the dict containing document metadata""" + formatted_fields = self.settings['FORMATTED_FIELDS'] + + output = {} + for docinfo in document.traverse(docutils.nodes.docinfo): + for element in docinfo.children: + if element.tagname == 'field': # custom fields (e.g. summary) + name_elem, body_elem = element.children + name = name_elem.astext() + if name in formatted_fields: + visitor = _SaneFieldBodyTranslator(document) + body_elem.walkabout(visitor) + value = visitor.astext() + else: + value = body_elem.astext() + elif element.tagname == 'authors': # author list + name = element.tagname + value = [element.astext() for element in element.children] + else: # standard fields (e.g. address) + name = element.tagname + value = element.astext() + name = name.lower() + + output[name] = self.process_metadata(name, value) + return output + + def _get_publisher(self, source_path): + extra_params = {'initial_header_level': '2', + 'syntax_highlight': 'short', + 'input_encoding': 'utf-8', + 'exit_status_level': 2, + 'embed_stylesheet': False} + user_params = self.settings.get('DOCUTILS_SETTINGS') + if user_params: + extra_params.update(user_params) + + pub = docutils.core.Publisher( + writer=SaneHtmlWriter(), + source_class=self.FileInput, + destination_class=docutils.io.StringOutput) + pub.set_components('standalone', 'restructuredtext', 'html') + pub.process_programmatic_settings(None, extra_params, None) + pub.set_source(source_path=source_path) + pub.publish(enable_exit_status=True) + return pub + def render_rst(value): extra_params = {'initial_header_level': '2', 'syntax_highlight': 'short', @@ -544,26 +607,110 @@ def dehyphenate(value, enable=None): if not enable: return value return value.replace('­', '') -def expand_link(link, content): +# TODO: merge into expand_link when 3.8 +# with https://github.com/getpelican/pelican/pull/2164 (or the _link_replacer +# part of it) is released +def expand_link_fn(link, content, fn): link_regex = r"""^ (?P)(?P) (?P{0}(?P.*)) $""".format(intrasite_link_regex) links = re.compile(link_regex, re.X) return links.sub( - lambda m: content._link_replacer(content.get_siteurl(), m), + lambda m: fn(content.get_siteurl(), m), link) +def expand_link(link, content): + return expand_link_fn(link, content, content._link_replacer) + +# The replacer() function is adapted from +# https://github.com/getpelican/pelican/blob/3.7.1/pelican/contents.py#L213 +# in order to be compatible with Pelican <= 3.7.1 that doesn't have it +# available publicly as _link_replacer +# TODO: remove when 3.8 with https://github.com/getpelican/pelican/pull/2164 +# (or the _link_replacer part of it) is released +def expand_link_pelican371(link, content): + def replacer(siteurl, m): + what = m.group('what') + value = urlparse(m.group('value')) + path = value.path + origin = m.group('path') + + # XXX Put this in a different location. + if what in {'filename', 'attach'}: + if path.startswith('/'): + path = path[1:] + else: + # relative to the source path of this content + path = content.get_relative_source_path( + os.path.join(content.relative_dir, path) + ) + + if path not in content._context['filenames']: + unquoted_path = path.replace('%20', ' ') + + if unquoted_path in content._context['filenames']: + path = unquoted_path + + linked_content = content._context['filenames'].get(path) + if linked_content: + if what == 'attach': + if isinstance(linked_content, Static): + linked_content.attach_to(content) + else: + logger.warning( + "%s used {attach} link syntax on a " + "non-static file. Use {filename} instead.", + content.get_relative_source_path()) + origin = '/'.join((siteurl, linked_content.url)) + origin = origin.replace('\\', '/') # for Windows paths. + else: + logger.warning( + "Unable to find `%s`, skipping url replacement.", + value.geturl(), extra={ + 'limit_msg': ("Other resources were not found " + "and their urls not replaced")}) + elif what == 'category': + origin = '/'.join((siteurl, Category(path, content.settings).url)) + elif what == 'tag': + origin = '/'.join((siteurl, Tag(path, content.settings).url)) + elif what == 'index': + origin = '/'.join((siteurl, content.settings['INDEX_SAVE_AS'])) + elif what == 'author': + origin = '/'.join((siteurl, Author(path, content.settings).url)) + else: + logger.warning( + "Replacement Indicator '%s' not recognized, " + "skipping replacement", + what) + + # keep all other parts, such as query, fragment, etc. + parts = list(value) + parts[2] = origin + origin = urlunparse(parts) + + return ''.join((m.group('markup'), m.group('quote'), origin, + m.group('quote'))) + + return expand_link_fn(link, content, replacer) + def expand_links(text, content): return content._update_content(text, content.get_siteurl()) def configure_pelican(pelicanobj): pelicanobj.settings['JINJA_FILTERS']['render_rst'] = render_rst - pelicanobj.settings['JINJA_FILTERS']['expand_link'] = expand_link pelicanobj.settings['JINJA_FILTERS']['expand_links'] = expand_links pelicanobj.settings['JINJA_FILTERS']['hyphenate'] = hyphenate pelicanobj.settings['JINJA_FILTERS']['dehyphenate'] = dehyphenate + # TODO: remove when 3.8 with https://github.com/getpelican/pelican/pull/2164 + # (or the _link_replacer part of it) is released + if not hasattr(Content, '_link_replacer'): + logger.warning('Unpatched Pelican <= 3.7.1 detected, monkey-patching for expand_link filter support') + pelicanobj.settings['JINJA_FILTERS']['expand_link'] = expand_link_pelican371 + else: + pelicanobj.settings['JINJA_FILTERS']['expand_link'] = expand_link + global enable_hyphenation, smart_quotes, hyphenation_lang, \ docutils_settings, intrasite_link_regex enable_hyphenation = pelicanobj.settings.get('HTMLSANITY_HYPHENATION', False) @@ -573,7 +720,13 @@ def configure_pelican(pelicanobj): intrasite_link_regex = pelicanobj.settings['INTRASITE_LINK_REGEX'] def add_reader(readers): - readers.reader_classes['rst'] = SaneRstReader + # TODO: remove when 3.8 with https://github.com/getpelican/pelican/pull/2163 + # is released + if not hasattr(RstReader, 'writer_class') or not hasattr(RstReader, 'field_body_translator_class'): + logger.warning('Unpatched Pelican <= 3.7.1 detected, monkey-patching for htmlsanity support') + readers.reader_classes['rst'] = SaneRstReaderPelican371 + else: + readers.reader_classes['rst'] = SaneRstReader def register(): pelican.signals.initialized.connect(configure_pelican) diff --git a/site/pelicanconf.py b/site/pelicanconf.py index ba1ffaf0..a96ee105 100644 --- a/site/pelicanconf.py +++ b/site/pelicanconf.py @@ -117,11 +117,11 @@ THEME = '../pelican-theme' THEME_STATIC_DIR = 'static' THEME_COLOR = '#22272e' CSS_FILES = ['https://fonts.googleapis.com/css?family=Source+Code+Pro:400,400i,600%7CSource+Sans+Pro:400,400i,600&subset=latin-ext', - STATIC_URL.format(path='static/m-dark.css'), - #STATIC_URL.format(path='static/m-debug.css') + '/static/m-dark.css', + #'/static/m-debug.css' ] #CSS_FILES = ['https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700%7CSource+Code+Pro:400,400i,600', - #STATIC_URL.format(path='static/m-light.css')] + #'/static/m-light.css'] FORMATTED_FIELDS = ['summary', 'landing'] @@ -150,3 +150,13 @@ TAGS_SAVE_AS = None # Not used SLUGIFY_SOURCE = 'basename' PATH_METADATA = '(?P.+).rst' + +# If https://github.com/getpelican/pelican/pull/2196 is not applied, all URLs +# would have / prepended twice, so removing it from the settings. +if True: + STATIC_URL = '{path}' + PAGE_URL = '{slug}' + ARTICLE_URL = '{category}/{slug}/' + AUTHOR_URL = 'author/{slug}/' + CATEGORY_URL = '{slug}/' + TAG_URL = 'tag/{slug}/' diff --git a/site/publishconf.py b/site/publishconf.py index 7645c1a7..16e95ce6 100644 --- a/site/publishconf.py +++ b/site/publishconf.py @@ -33,7 +33,7 @@ OUTPUT_PATH = 'published/' DELETE_OUTPUT_DIRECTORY = True CSS_FILES = ['https://fonts.googleapis.com/css?family=Source+Code+Pro:400,400i,600%7CSource+Sans+Pro:400,400i,600&subset=latin-ext', - STATIC_URL.format(path='static/m-dark.compiled.css')] + 'http://mcss.mosra.cz/static/m-dark.compiled.css'] PAGE_URL = 'http://mcss.mosra.cz/{slug}/' ARTICLE_URL = 'http://mcss.mosra.cz/{category}/{slug}/' -- 2.30.2