Skip to content

Commit

Permalink
m.code: ability to apply filters before/after the code is highlighted.
Browse files Browse the repository at this point in the history
  • Loading branch information
mosra committed Sep 3, 2019
1 parent e55ccd1 commit a246040
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 8 deletions.
80 changes: 80 additions & 0 deletions doc/plugins/math-and-code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ plugin assumes presence of `m.htmlsanity <{filename}/plugins/htmlsanity.rst>`_.
.. code:: python
PLUGINS += ['m-htmlsanity', 'm.code']
M_CODE_FILTERS_PRE = []
M_CODE_FILTERS_POST = []
For the Python doc theme, it's enough to mention it in :py:`PLUGINS`. The
`m.htmlsanity`_ plugin is available always, no need to mention it explicitly:
Expand Down Expand Up @@ -445,3 +447,81 @@ immediately followed by background color specification (the
See the `m.components <{filename}/plugins/components.rst#code-math-and-graph-figure>`__
plugin for details about code figures using the :rst:`.. code-figure::`
directive.

`Filters`_
----------

It's possible to supply filters that get applied both before and after a
code snippet is rendered using the :py:`M_CODE_FILTERS_PRE` and
:py:`M_CODE_FILTERS_POST` options. It's a dict with keys being the lexer
name [1]_ and values being filter functions. Each function that gets string as
an input and is expected to return a modified string. In the following example,
all CSS code snippets have the hexadecimal color literals annotated with a
`color swatch <{filename}/css/components.rst#color-swatches-in-code-snippets>`_:

.. code:: py
:class: m-console-wrap
import re
_css_colors_src = re.compile(r"""<span class="mh">#(?P<hex>[0-9a-f]{6})</span>""")
_css_colors_dst = r"""<span class="mh">#\g<hex><span class="m-code-color" style="background-color: #\g<hex>;"></span></span>"""
M_CODE_FILTERS_POST = {
'CSS': lambda code: _css_colors_src.sub(_css_colors_dst, code)
}
.. code-figure::

.. code:: rst
.. code:: css
p.green {
color: #3bd267;
}
.. code:: css
p.green {
color: #3bd267;
}
In the above case, the filter gets applied globally to all code snippets of
given language. Sometimes it might be desirable to apply a filter only to
specific code snippet --- in that case, the dict key is a tuple of
:py:`(lexer, filter)` where the second item is a filter name. This filter name
is then referenced from the :rst:`:filters:` option of the :rst:`.. code::` and
:rst:`.. include::` directives as well as the inline :rst:`:code:` text role.
Multiple filters can be specified when separated by spaces.

.. code:: py
M_CODE_FILTERS_PRE = {
('C++', 'codename'): lambda code: code.replace('DirtyMess', 'P300'),
('C++', 'fix_typography'): lambda code: code.replace(' :', ':'),
}
.. code-figure::

.. code:: rst
.. code:: cpp
:filters: codename fix_typography
for(auto& a : DirtyMess::managedEntities()) {
// ...
}
.. code:: cpp
:filters: codename fix_typography
for(auto& a : DirtyMess::managedEntities()) {
// ...
}
.. [1] In order to have an unique mapping, the filters can't use the aliases
--- for example C++ code can be highlighted using either ``c++`` or ``cpp``
as a language name and the dict would need to have an entry for each. An unique lexer name is the :py:`name` field used in the particular lexer
source, you can also see the names in the language dropdown on the
`official website <http://pygments.org/demo/>`_.
61 changes: 54 additions & 7 deletions plugins/m/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@

import ansilexer

def _highlight(code, language, options, is_block):
filters_pre = None
filters_post = None

def _highlight(code, language, options, *, is_block, filters=[]):
# Use our own lexer for ANSI
if language == 'ansi':
lexer = ansilexer.AnsiLexer()
Expand All @@ -63,9 +66,28 @@ def _highlight(code, language, options, is_block):
formatter = ansilexer.HtmlAnsiFormatter(**options)
else:
formatter = HtmlFormatter(nowrap=True, **options)

global filters_pre
# First apply local pre filters, if any
for filter in filters:
f = filters_pre.get((lexer.name, filter))
if f: code = f(code)
# Then a global pre filter, if any
f = filters_pre.get(lexer.name)
if f: code = f(code)

parsed = highlight(code, lexer, formatter).rstrip()
if not is_block: parsed.lstrip()

global filters_post
# First apply local post filters, if any
for filter in filters:
f = filters_post.get((lexer.name, filter))
if f: parsed = f(parsed)
# Then a global post filter, if any
f = filters_post.get(lexer.name)
if f: parsed = f(parsed)

return class_, parsed

class Code(Directive):
Expand All @@ -74,7 +96,8 @@ class Code(Directive):
final_argument_whitespace = True
option_spec = {
'hl_lines': directives.unchanged,
'class': directives.class_option
'class': directives.class_option,
'filters': directives.unchanged
}
has_content = True

Expand All @@ -87,7 +110,9 @@ def run(self):
classes += self.options['classes']
del self.options['classes']

class_, highlighted = _highlight('\n'.join(self.content), self.arguments[0], self.options, is_block=True)
filters = self.options.pop('filters', '').split()

class_, highlighted = _highlight('\n'.join(self.content), self.arguments[0], self.options, is_block=True, filters=filters)
classes += [class_]

content = nodes.raw('', highlighted, format='html')
Expand All @@ -96,6 +121,11 @@ def run(self):
return [pre]

class Include(docutils.parsers.rst.directives.misc.Include):
option_spec = {
**docutils.parsers.rst.directives.misc.Include.option_spec,
'filters': directives.unchanged
}

def run(self):
"""
Verbatim copy of docutils.parsers.rst.directives.misc.Include.run()
Expand Down Expand Up @@ -199,7 +229,9 @@ def code(role, rawtext, text, lineno, inliner, options={}, content=[]):
# Not sure why language is duplicated in classes?
if language in classes: classes.remove(language)

class_, highlighted = _highlight(utils.unescape(text), language, options, is_block=False)
filters = options.pop('filters', '').split()

class_, highlighted = _highlight(utils.unescape(text), language, options, is_block=False, filters=filters)
classes += [class_]

content = nodes.raw('', highlighted, format='html')
Expand All @@ -208,14 +240,29 @@ def code(role, rawtext, text, lineno, inliner, options={}, content=[]):
return [node], []

code.options = {'class': directives.class_option,
'language': directives.unchanged}
'language': directives.unchanged,
'filters': directives.unchanged}

def register_mcss(**kwargs):
def register_mcss(mcss_settings, **kwargs):
rst.directives.register_directive('code', Code)
rst.directives.register_directive('include', Include)
rst.roles.register_canonical_role('code', code)

global filters_pre, filters_post
filters_pre = mcss_settings.get('M_CODE_FILTERS_PRE', {})
filters_post = mcss_settings.get('M_CODE_FILTERS_POST', {})

# Below is only Pelican-specific functionality. If Pelican is not found, these
# do nothing.

register = register_mcss # for Pelican
def _pelican_configure(pelicanobj):
settings = {}
for key in ['M_CODE_FILTERS_PRE', 'M_CODE_FILTERS_POST']:
if key in pelicanobj.settings: settings[key] = pelicanobj.settings[key]

register_mcss(mcss_settings=settings)

def register(): # for Pelican
import pelican.signals

pelican.signals.initialized.connect(_pelican_configure)
17 changes: 17 additions & 0 deletions plugins/m/test/code/page.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions plugins/m/test/code/page.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,38 @@ Don't trim leading spaces in blocks:
nope();
return false;
}

`Filters`_
==========

.. role:: css(code)
:language: css

Applied by default, adding typographically correct spaces before and a color
swatch after --- and for inline as well: :css:`p{ color:#ff3366; }`

.. code:: css
p{
color:#ff3366;
}
.. role:: css-filtered(code)
:language: css
:filters: lowercase replace_colors

Applied explicity and then by default --- and for inline as well:
:css-filtered:`P{ COLOR:#C0FFEE; }`

.. code:: css
:filters: lowercase replace_colors
P{
COLOR:#C0FFEE;
}
Includes too:

.. include:: style.css
:code: css
:filters: lowercase replace_colors
3 changes: 3 additions & 0 deletions plugins/m/test/code/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
P{
COLOR:#C0FFEE;
}
19 changes: 18 additions & 1 deletion plugins/m/test/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@
# DEALINGS IN THE SOFTWARE.
#

import re

from . import PelicanPluginTestCase

_css_colors_src = re.compile(r"""<span class="mh">#(?P<hex>[0-9a-f]{6})</span>""")
_css_colors_dst = r"""<span class="mh">#\g<hex><span class="m-code-color" style="background-color: #\g<hex>;"></span></span>"""

def _add_color_swatch(str):
return _css_colors_src.sub(_css_colors_dst, str)

class Code(PelicanPluginTestCase):
def __init__(self, *args, **kwargs):
super().__init__(__file__, '', *args, **kwargs)
Expand All @@ -33,7 +41,16 @@ def test(self):
# Need Source Code Pro for code
'M_CSS_FILES': ['https://fonts.googleapis.com/css?family=Source+Code+Pro:400,400i,600%7CSource+Sans+Pro:400,400i,600,600i',
'static/m-dark.css'],
'PLUGINS': ['m.htmlsanity', 'm.code']
'PLUGINS': ['m.htmlsanity', 'm.code'],
'M_CODE_FILTERS_PRE': {
'CSS': lambda str: str.replace(':', ': ').replace('{', ' {'),
('CSS', 'lowercase'): lambda str: str.lower(),
('CSS', 'uppercase'): lambda str: str.upper(), # not used
},
'M_CODE_FILTERS_POST': {
'CSS': _add_color_swatch,
('CSS', 'replace_colors'): lambda str: str.replace('#c0ffee', '#3bd267')
},
})

self.assertEqual(*self.actual_expected_contents('page.html'))
14 changes: 14 additions & 0 deletions site/pelicanconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# DEALINGS IN THE SOFTWARE.
#

import re
import shutil
import logging

Expand Down Expand Up @@ -187,6 +188,19 @@
logging.warning("LaTeX not found, fallback to rendering math as code")
M_MATH_RENDER_AS_CODE = True

# Used by the m.code plugin docs

_css_colors_src = re.compile(r"""<span class="mh">#(?P<hex>[0-9a-f]{6})</span>""")
_css_colors_dst = r"""<span class="mh">#\g<hex><span class="m-code-color" style="background-color: #\g<hex>;"></span></span>"""

M_CODE_FILTERS_PRE = {
('C++', 'codename'): lambda code: code.replace('DirtyMess', 'P300::V1'),
('C++', 'fix_typography'): lambda code: code.replace(' :', ':'),
}
M_CODE_FILTERS_POST = {
'CSS': lambda code: _css_colors_src.sub(_css_colors_dst, code)
}

DIRECT_TEMPLATES = ['archives']

PAGE_URL = '{slug}/'
Expand Down

0 comments on commit a246040

Please sign in to comment.