Skip to content

Commit

Permalink
Support for custom Pygments formatter
Browse files Browse the repository at this point in the history
This adds configuration support for using a custom Pygments formatter,
either by giving the string name, or a custom formatter class (or
callable).
  • Loading branch information
sharat87 committed May 9, 2022
1 parent 93d17b9 commit 9d90d47
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 4 deletions.
1 change: 1 addition & 0 deletions .spell-dict
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,4 @@ plugin
plugins
configs
pre
formatters
8 changes: 7 additions & 1 deletion docs/change_log/release-3.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<div>` 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:
10 changes: 10 additions & 0 deletions docs/extensions/code_hilite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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/
16 changes: 14 additions & 2 deletions markdown/extensions/codehilite.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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
Expand Down Expand Up @@ -99,6 +100,7 @@ def __init__(self, src, **options):
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)
Expand Down Expand Up @@ -139,7 +141,13 @@ def hilite(self, shebang=True):
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
Expand Down Expand Up @@ -279,7 +287,11 @@ def __init__(self, **kwargs):
'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():
Expand Down
130 changes: 129 additions & 1 deletion tests/test_syntax/extensions/test_fenced_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@

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
Expand Down Expand Up @@ -800,7 +802,7 @@ def testFencedLanguageAttrNoclasses(self):
extensions=['codehilite', 'fenced_code']
)

def testFencedMultpleBlocksSameStyle(self):
def testFencedMultipleBlocksSameStyle(self):
if has_pygments:
# See also: https://github.com/Python-Markdown/markdown/issues/1240
expected = (
Expand Down Expand Up @@ -844,3 +846,129 @@ def testFencedMultpleBlocksSameStyle(self):
'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, '<code>'
for i, t in source:
if i == 1:
t += '<br>'
yield i, t
yield 0, '</code>'

expected = '''
<div class="codehilite"><code>hello world
<br>hello another world
<br></code></div>
'''

else:
CustomFormatter = None
expected = '''
<pre class="codehilite"><code>hello world
hello another world
</code></pre>
'''

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 = '''
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<g font-family="monospace" font-size="14px">
<text x="0" y="14" xml:space="preserve">hello&#160;world</text>
<text x="0" y="33" xml:space="preserve">hello&#160;another&#160;world</text>
<text x="0" y="52" xml:space="preserve"></text></g></svg>
'''

else:
expected = '''
<pre class="codehilite"><code>hello world
hello another world
</code></pre>
'''

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 = '''
<div class="codehilite"><pre><span></span><code>hello world
hello another world
</code></pre></div>
'''

else:
expected = '''
<pre class="codehilite"><code>hello world
hello another world
</code></pre>
'''

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'
]
)

0 comments on commit 9d90d47

Please sign in to comment.