chiark / gitweb /
m.code: ability to apply filters before/after the code is highlighted.
authorVladimír Vondruš <mosra@centrum.cz>
Tue, 3 Sep 2019 21:38:47 +0000 (23:38 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Tue, 3 Sep 2019 21:38:47 +0000 (23:38 +0200)
doc/plugins/math-and-code.rst
plugins/m/code.py
plugins/m/test/code/page.html
plugins/m/test/code/page.rst
plugins/m/test/code/style.css [new file with mode: 0644]
plugins/m/test/test_code.py
site/pelicanconf.py

index aa80350729c02c69561483ec0d7313404c5a26f4..dc5baf99e8af45d9fbd67cbfa1acb1faa0a1a0f4 100644 (file)
@@ -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:
@@ -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/>`_.
index 50ff5a2c15f1d67b9593958831addf4abf086791..9142a9f2f12cb3608593a2c99f44b034cd9251bd 100644 (file)
@@ -42,7 +42,10 @@ logger = logging.getLogger(__name__)
 
 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()
@@ -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):
@@ -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
 
@@ -87,7 +110,9 @@ class Code(Directive):
             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')
@@ -96,6 +121,11 @@ class Code(Directive):
         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()
@@ -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')
@@ -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)
index e806d052b0b4d35dffefb1db37238dc560dd3828..ba58943b1027d2576151631980be0c492175b464 100644 (file)
@@ -47,6 +47,23 @@ Leading zeros: <span style="color: #0f7403; background-color: #0f7403">&zwnj;▒
 <pre class="m-code">        <span class="n">nope</span><span class="p">();</span>
     <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
 <span class="p">}</span></pre>
+<section id="filters">
+<h2><a href="#filters">Filters</a></h2>
+<p>Applied by default, adding typographically correct spaces before and a color
+swatch after --- and for inline as well: <code class="m-code"><span class="nt">p</span> <span class="p">{</span> <span class="k">color</span><span class="p">:</span> <span class="mh">#ff3366<span class="m-code-color" style="background-color: #ff3366;"></span></span><span class="p">;</span> <span class="p">}</span></code></p>
+<pre class="m-code"><span class="nt">p</span> <span class="p">{</span>
+    <span class="k">color</span><span class="p">:</span> <span class="mh">#ff3366<span class="m-code-color" style="background-color: #ff3366;"></span></span><span class="p">;</span>
+<span class="p">}</span></pre>
+<p>Applied explicity and then by default --- and for inline as well:
+<code class="css-filtered m-code"><span class="nt">p</span> <span class="p">{</span> <span class="k">color</span><span class="p">:</span> <span class="mh">#3bd267<span class="m-code-color" style="background-color: #3bd267;"></span></span><span class="p">;</span> <span class="p">}</span></code></p>
+<pre class="m-code"><span class="nt">p</span> <span class="p">{</span>
+    <span class="k">color</span><span class="p">:</span> <span class="mh">#3bd267<span class="m-code-color" style="background-color: #3bd267;"></span></span><span class="p">;</span>
+<span class="p">}</span></pre>
+<p>Includes too:</p>
+<pre class="m-code"><span class="nt">p</span> <span class="p">{</span>
+    <span class="k">color</span><span class="p">:</span> <span class="mh">#3bd267<span class="m-code-color" style="background-color: #3bd267;"></span></span><span class="p">;</span>
+<span class="p">}</span></pre>
+</section>
 <!-- /content -->
       </div>
     </div>
index a4292a98720ecfbd0679d1b0d9ca88d80c53b795..b548348f8379bf191255d85f6f339710e69b173f 100644 (file)
@@ -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
diff --git a/plugins/m/test/code/style.css b/plugins/m/test/code/style.css
new file mode 100644 (file)
index 0000000..6d24c8e
--- /dev/null
@@ -0,0 +1,3 @@
+P{
+    COLOR:#C0FFEE;
+}
index b49998bb2ff7bfa1a545973661fe20b7a4f9c243..2b6ed0c92c49a76d8e50a15552382d3fcc6760e5 100644 (file)
 #   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)
@@ -33,7 +41,16 @@ class Code(PelicanPluginTestCase):
             # 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'))
index 19e9408a4811c7e390eb73fdbdc500a211cf4e24..3e6008c2ca0566e0ddfe5244f0748bf7e8c28a41 100644 (file)
@@ -22,6 +22,7 @@
 #   DEALINGS IN THE SOFTWARE.
 #
 
+import re
 import shutil
 import logging
 
@@ -187,6 +188,19 @@ if not shutil.which('latex'):
     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}/'