From 9d90d47157f4348ad70f1a070571de87efbc6cbc Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Mon, 9 May 2022 18:26:10 +0530 Subject: Support for custom Pygments formatter This adds configuration support for using a custom Pygments formatter, either by giving the string name, or a custom formatter class (or callable). --- .spell-dict | 1 + docs/change_log/release-3.4.md | 8 +- docs/extensions/code_hilite.md | 10 ++ markdown/extensions/codehilite.py | 16 ++- tests/test_syntax/extensions/test_fenced_code.py | 130 ++++++++++++++++++++++- 5 files changed, 161 insertions(+), 4 deletions(-) diff --git a/.spell-dict b/.spell-dict index fbe4865..6408100 100644 --- a/.spell-dict +++ b/.spell-dict @@ -160,3 +160,4 @@ plugin plugins configs pre +formatters diff --git a/docs/change_log/release-3.4.md b/docs/change_log/release-3.4.md index 025d443..6dc8401 100644 --- a/docs/change_log/release-3.4.md +++ b/docs/change_log/release-3.4.md @@ -32,7 +32,7 @@ In addition, tests were moved to the modern test environment. ## New features -The following new features have been included in the 3.3 release: +The following new features have been included in the 3.4 release: * Use `style` attribute in tables for alignment instead of `align` for better CSS inter-operation. The old behavior is available by setting `use_align_attribute=True` when @@ -55,6 +55,12 @@ The following new features have been included in the 3.3 release: parameter which can be used to set the CSS class(es) on the `
` that contains the Table of Contents (#1224). +* The Codehilite extension now supports a `pygments_formatter` option that can be set to + use a custom formatter class with Pygments. + - If set to a string like `'html'`, we get the default formatter by that name. + - If set to a class (or any callable), it is called with all the options to get a + formatter instance. + ## Bug fixes The following bug fixes are included in the 3.4 release: diff --git a/docs/extensions/code_hilite.md b/docs/extensions/code_hilite.md index caaf011..6fa6190 100644 --- a/docs/extensions/code_hilite.md +++ b/docs/extensions/code_hilite.md @@ -234,6 +234,15 @@ The following options are provided to configure the output: This option only applies when `use_pygments` is `False` as Pygments does not provide an option to include a language prefix. +* **`pygments_formatter`**{ #pygments_formatter }: + This option can be used to change the Pygments formatter used for highlighting the code blocks. By default, this + is set to the string `'html'`, which means it'll use the default `HtmlFormatter` provided by Pygments. + + This can be set to a string representing any of the other default formatters, or set to a formatter class (or + any callable). + + To see what formatters are available and how to subclass an existing formatter, please visit [Pygments + documentation on this topic][pygments formatters]. * Any other Pygments' options: @@ -250,3 +259,4 @@ markdown.markdown(some_text, extensions=['codehilite']) [html formatter]: https://pygments.org/docs/formatters/#HtmlFormatter [lexer]: https://pygments.org/docs/lexers/ [spec]: https://www.w3.org/TR/html5/text-level-semantics.html#the-code-element +[pygments formatters]: https://pygments.org/docs/formatters/ diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py index a768b73..b92ebdc 100644 --- a/markdown/extensions/codehilite.py +++ b/markdown/extensions/codehilite.py @@ -23,6 +23,7 @@ try: # pragma: no cover from pygments import highlight from pygments.lexers import get_lexer_by_name, guess_lexer from pygments.formatters import get_formatter_by_name + from pygments.util import ClassNotFound pygments = True except ImportError: # pragma: no cover pygments = False @@ -99,6 +100,7 @@ class CodeHilite: self.guess_lang = options.pop('guess_lang', True) self.use_pygments = options.pop('use_pygments', True) self.lang_prefix = options.pop('lang_prefix', 'language-') + self.pygments_formatter = options.pop('pygments_formatter', 'html') if 'linenos' not in options: options['linenos'] = options.pop('linenums', None) @@ -139,7 +141,13 @@ class CodeHilite: lexer = get_lexer_by_name('text', **self.options) except ValueError: # pragma: no cover lexer = get_lexer_by_name('text', **self.options) - formatter = get_formatter_by_name('html', **self.options) + if isinstance(self.pygments_formatter, str): + try: + formatter = get_formatter_by_name(self.pygments_formatter, **self.options) + except ClassNotFound: + formatter = get_formatter_by_name('html', **self.options) + else: + formatter = self.pygments_formatter(**self.options) return highlight(self.src, lexer, formatter) else: # just escape and build markup usable by JS highlighting libs @@ -279,7 +287,11 @@ class CodeHiliteExtension(Extension): 'lang_prefix': [ 'language-', 'Prefix prepended to the language when use_pygments is false. Default: "language-"' - ] + ], + 'pygments_formatter': ['html', + 'Use a specific formatter for Pygments hilighting.' + 'Default: "html"', + ], } for key, value in kwargs.items(): diff --git a/tests/test_syntax/extensions/test_fenced_code.py b/tests/test_syntax/extensions/test_fenced_code.py index 2cdde98..f8c3e91 100644 --- a/tests/test_syntax/extensions/test_fenced_code.py +++ b/tests/test_syntax/extensions/test_fenced_code.py @@ -21,10 +21,12 @@ License: BSD (see LICENSE.md for details). from markdown.test_tools import TestCase import markdown +import markdown.extensions.codehilite import os try: import pygments # noqa + import pygments.formatters # noqa has_pygments = True except ImportError: has_pygments = False @@ -800,7 +802,7 @@ class TestFencedCodeWithCodehilite(TestCase): extensions=['codehilite', 'fenced_code'] ) - def testFencedMultpleBlocksSameStyle(self): + def testFencedMultipleBlocksSameStyle(self): if has_pygments: # See also: https://github.com/Python-Markdown/markdown/issues/1240 expected = ( @@ -844,3 +846,129 @@ class TestFencedCodeWithCodehilite(TestCase): 'fenced_code' ] ) + + def testCustomPygmentsFormatter(self): + if has_pygments: + class CustomFormatter(pygments.formatters.HtmlFormatter): + def wrap(self, source, outfile): + return self._wrap_div(self._wrap_code(source)) + + def _wrap_code(self, source): + yield 0, '' + for i, t in source: + if i == 1: + t += '
' + yield i, t + yield 0, '
' + + expected = ''' +
hello world +
hello another world +
+ ''' + + else: + CustomFormatter = None + expected = ''' +
hello world
+            hello another world
+            
+ ''' + + self.assertMarkdownRenders( + self.dedent( + ''' + ``` + hello world + hello another world + ``` + ''' + ), + self.dedent( + expected + ), + extensions=[ + markdown.extensions.codehilite.CodeHiliteExtension( + pygments_formatter=CustomFormatter, + guess_lang=False, + ), + 'fenced_code' + ] + ) + + def testSvgCustomPygmentsFormatter(self): + if has_pygments: + expected = ''' + + + + + hello world + hello another world + + ''' + + else: + expected = ''' +
hello world
+            hello another world
+            
+ ''' + + self.assertMarkdownRenders( + self.dedent( + ''' + ``` + hello world + hello another world + ``` + ''' + ), + self.dedent( + expected + ), + extensions=[ + markdown.extensions.codehilite.CodeHiliteExtension( + pygments_formatter='svg', + linenos=False, + guess_lang=False, + ), + 'fenced_code' + ] + ) + + def testInvalidCustomPygmentsFormatter(self): + if has_pygments: + expected = ''' +
hello world
+            hello another world
+            
+ ''' + + else: + expected = ''' +
hello world
+            hello another world
+            
+ ''' + + self.assertMarkdownRenders( + self.dedent( + ''' + ``` + hello world + hello another world + ``` + ''' + ), + self.dedent( + expected + ), + extensions=[ + markdown.extensions.codehilite.CodeHiliteExtension( + pygments_formatter='invalid', + guess_lang=False, + ), + 'fenced_code' + ] + ) -- cgit v1.2.1