markdown.py
225 lines
| 6.6 KiB
| text/x-python
|
PythonLexer
Jonathan Frederic
|
r10676 | """Markdown filters | ||
Thomas Kluyver
|
r17259 | |||
Jonathan Frederic
|
r10676 | This file contains a collection of utility filters for dealing with | ||
markdown within Jinja templates. | ||||
Jonathan Frederic
|
r10436 | """ | ||
Thomas Kluyver
|
r17259 | # Copyright (c) IPython Development Team. | ||
Jonathan Frederic
|
r10436 | # Distributed under the terms of the Modified BSD License. | ||
from __future__ import print_function | ||||
# Stdlib imports | ||||
MinRK
|
r14206 | import os | ||
Jonathan Frederic
|
r10436 | import subprocess | ||
MinRK
|
r14203 | from io import TextIOWrapper, BytesIO | ||
Thomas Kluyver
|
r17251 | import re | ||
Jonathan Frederic
|
r10436 | |||
Thomas Kluyver
|
r17249 | import mistune | ||
from pygments import highlight | ||||
from pygments.lexers import get_lexer_by_name | ||||
from pygments.formatters import HtmlFormatter | ||||
Thomas Kluyver
|
r17258 | from pygments.util import ClassNotFound | ||
Thomas Kluyver
|
r17247 | |||
MinRK
|
r14203 | # IPython imports | ||
MinRK
|
r11268 | from IPython.nbconvert.utils.pandoc import pandoc | ||
MinRK
|
r14203 | from IPython.nbconvert.utils.exceptions import ConversionException | ||
Thomas Kluyver
|
r17398 | from IPython.utils.decorators import undoc | ||
Jonathan Frederic
|
r15692 | from IPython.utils.process import get_output_error_code | ||
MinRK
|
r14203 | from IPython.utils.py3compat import cast_bytes | ||
Jonathan Frederic
|
r15692 | from IPython.utils.version import check_version | ||
MinRK
|
r11268 | |||
Thomas Kluyver
|
r17259 | |||
MinRK
|
r14206 | marked = os.path.join(os.path.dirname(__file__), "marked.js") | ||
Jonathan Frederic
|
r15697 | _node = None | ||
Jonathan Frederic
|
r10485 | |||
Brian E. Granger
|
r11088 | __all__ = [ | ||
MinRK
|
r11268 | 'markdown2html', | ||
MinRK
|
r14203 | 'markdown2html_pandoc', | ||
'markdown2html_marked', | ||||
Thomas Kluyver
|
r17247 | 'markdown2html_mistune', | ||
Brian E. Granger
|
r11088 | 'markdown2latex', | ||
MinRK
|
r14203 | 'markdown2rst', | ||
Brian E. Granger
|
r11088 | ] | ||
MinRK
|
r14206 | class NodeJSMissing(ConversionException): | ||
"""Exception raised when node.js is missing.""" | ||||
MinRK
|
r14203 | pass | ||
Jonathan Frederic
|
r10676 | def markdown2latex(source): | ||
Jonathan Frederic
|
r10436 | """Convert a markdown string to LaTeX via pandoc. | ||
This function will raise an error if pandoc is not installed. | ||||
Any error messages generated by pandoc are printed to stderr. | ||||
Parameters | ||||
---------- | ||||
Jonathan Frederic
|
r10676 | source : string | ||
Jonathan Frederic
|
r10436 | Input string, assumed to be valid markdown. | ||
Returns | ||||
------- | ||||
out : string | ||||
Output as returned by pandoc. | ||||
""" | ||||
MinRK
|
r11268 | return pandoc(source, 'markdown', 'latex') | ||
Jonathan Frederic
|
r10436 | |||
Thomas Kluyver
|
r17398 | |||
@undoc | ||||
Thomas Kluyver
|
r17251 | class MathBlockGrammar(mistune.BlockGrammar): | ||
Thomas Kluyver
|
r17259 | block_math = re.compile("^\$\$(.*?)\$\$", re.DOTALL) | ||
latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", | ||||
re.DOTALL) | ||||
Thomas Kluyver
|
r17251 | |||
Thomas Kluyver
|
r17398 | @undoc | ||
Thomas Kluyver
|
r17251 | class MathBlockLexer(mistune.BlockLexer): | ||
Thomas Kluyver
|
r17259 | default_features = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_features | ||
Thomas Kluyver
|
r17251 | |||
def __init__(self, rules=None, **kwargs): | ||||
if rules is None: | ||||
rules = MathBlockGrammar() | ||||
super(MathBlockLexer, self).__init__(rules, **kwargs) | ||||
def parse_block_math(self, m): | ||||
"""Parse a $$math$$ block""" | ||||
self.tokens.append({ | ||||
'type': 'block_math', | ||||
'text': m.group(1) | ||||
}) | ||||
Thomas Kluyver
|
r17259 | def parse_latex_environment(self, m): | ||
self.tokens.append({ | ||||
'type': 'latex_environment', | ||||
'name': m.group(1), | ||||
'text': m.group(2) | ||||
}) | ||||
Thomas Kluyver
|
r17254 | |||
Thomas Kluyver
|
r17398 | @undoc | ||
Thomas Kluyver
|
r17251 | class MathInlineGrammar(mistune.InlineGrammar): | ||
math = re.compile("^\$(.+?)\$") | ||||
Thomas Kluyver
|
r17398 | @undoc | ||
Thomas Kluyver
|
r17251 | class MathInlineLexer(mistune.InlineLexer): | ||
default_features = ['math'] + mistune.InlineLexer.default_features | ||||
def __init__(self, renderer, rules=None, **kwargs): | ||||
if rules is None: | ||||
rules = MathInlineGrammar() | ||||
super(MathInlineLexer, self).__init__(renderer, rules, **kwargs) | ||||
def output_math(self, m): | ||||
Thomas Kluyver
|
r17259 | return self.renderer.inline_math(m.group(1)) | ||
Thomas Kluyver
|
r17251 | |||
Thomas Kluyver
|
r17398 | @undoc | ||
Thomas Kluyver
|
r17251 | class MarkdownWithMath(mistune.Markdown): | ||
def __init__(self, renderer, **kwargs): | ||||
if 'inline' not in kwargs: | ||||
MinRK
|
r17520 | kwargs['inline'] = MathInlineLexer | ||
Thomas Kluyver
|
r17251 | if 'block' not in kwargs: | ||
MinRK
|
r17520 | kwargs['block'] = MathBlockLexer | ||
Thomas Kluyver
|
r17251 | super(MarkdownWithMath, self).__init__(renderer, **kwargs) | ||
def parse_block_math(self): | ||||
return self.renderer.block_math(self.token['text']) | ||||
Thomas Kluyver
|
r17249 | |||
Thomas Kluyver
|
r17259 | def parse_latex_environment(self): | ||
return self.renderer.latex_environment(self.token['name'], self.token['text']) | ||||
Thomas Kluyver
|
r17398 | @undoc | ||
Thomas Kluyver
|
r17256 | class IPythonRenderer(mistune.Renderer): | ||
Thomas Kluyver
|
r17249 | def block_code(self, code, lang): | ||
Thomas Kluyver
|
r17258 | if lang: | ||
try: | ||||
lexer = get_lexer_by_name(lang, stripall=True) | ||||
except ClassNotFound: | ||||
code = lang + '\n' + code | ||||
lang = None | ||||
Thomas Kluyver
|
r17249 | if not lang: | ||
return '\n<pre><code>%s</code></pre>\n' % \ | ||||
mistune.escape(code) | ||||
Thomas Kluyver
|
r17258 | |||
Thomas Kluyver
|
r17249 | formatter = HtmlFormatter() | ||
return highlight(code, lexer, formatter) | ||||
Thomas Kluyver
|
r17248 | |||
Thomas Kluyver
|
r17251 | # Pass math through unaltered - mathjax does the rendering in the browser | ||
def block_math(self, text): | ||||
return '$$%s$$' % text | ||||
Thomas Kluyver
|
r17259 | def latex_environment(self, name, text): | ||
return r'\begin{%s}%s\end{%s}' % (name, text, name) | ||||
Thomas Kluyver
|
r17251 | def inline_math(self, text): | ||
return '$%s$' % text | ||||
Thomas Kluyver
|
r17247 | def markdown2html_mistune(source): | ||
Thomas Kluyver
|
r17249 | """Convert a markdown string to HTML using mistune""" | ||
Thomas Kluyver
|
r17256 | return MarkdownWithMath(renderer=IPythonRenderer()).render(source) | ||
Thomas Kluyver
|
r17247 | |||
MinRK
|
r14203 | def markdown2html_pandoc(source): | ||
MinRK
|
r11268 | """Convert a markdown string to HTML via pandoc""" | ||
MinRK
|
r11294 | return pandoc(source, 'markdown', 'html', extra_args=['--mathjax']) | ||
MinRK
|
r11268 | |||
Thomas Kluyver
|
r17249 | def _find_nodejs(): | ||
global _node | ||||
if _node is None: | ||||
# prefer md2html via marked if node.js >= 0.9.12 is available | ||||
# node is called nodejs on debian, so try that first | ||||
_node = 'nodejs' | ||||
if not _verify_node(_node): | ||||
_node = 'node' | ||||
return _node | ||||
MinRK
|
r14203 | def markdown2html_marked(source, encoding='utf-8'): | ||
"""Convert a markdown string to HTML via marked""" | ||||
Thomas Kluyver
|
r17249 | command = [_find_nodejs(), marked] | ||
MinRK
|
r14203 | try: | ||
p = subprocess.Popen(command, | ||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE | ||||
) | ||||
except OSError as e: | ||||
MinRK
|
r14206 | raise NodeJSMissing( | ||
MinRK
|
r14203 | "The command '%s' returned an error: %s.\n" % (" ".join(command), e) + | ||
MinRK
|
r14206 | "Please check that Node.js is installed." | ||
MinRK
|
r14203 | ) | ||
out, _ = p.communicate(cast_bytes(source, encoding)) | ||||
out = TextIOWrapper(BytesIO(out), encoding, 'replace').read() | ||||
return out.rstrip('\n') | ||||
Thomas Kluyver
|
r17249 | # The mistune renderer is the default, because it's simple to depend on it | ||
Thomas Kluyver
|
r17257 | markdown2html = markdown2html_mistune | ||
Thomas Kluyver
|
r17249 | |||
Jonathan Frederic
|
r10676 | def markdown2rst(source): | ||
Paul Ivanov
|
r15880 | """Convert a markdown string to ReST via pandoc. | ||
Jonathan Frederic
|
r10436 | |||
This function will raise an error if pandoc is not installed. | ||||
Any error messages generated by pandoc are printed to stderr. | ||||
Parameters | ||||
---------- | ||||
Jonathan Frederic
|
r10676 | source : string | ||
Jonathan Frederic
|
r10436 | Input string, assumed to be valid markdown. | ||
Returns | ||||
------- | ||||
out : string | ||||
Output as returned by pandoc. | ||||
""" | ||||
MinRK
|
r11268 | return pandoc(source, 'markdown', 'rst') | ||
Jonathan Frederic
|
r15691 | def _verify_node(cmd): | ||
Jonathan Frederic
|
r15692 | """Verify that the node command exists and is at least the minimum supported | ||
Jonathan Frederic
|
r15691 | version of node. | ||
Parameters | ||||
---------- | ||||
cmd : string | ||||
Node command to verify (i.e 'node').""" | ||||
MinRK
|
r15437 | try: | ||
Jonathan Frederic
|
r15692 | out, err, return_code = get_output_error_code([cmd, '--version']) | ||
except OSError: | ||||
Jonathan Frederic
|
r15693 | # Command not found | ||
Jonathan Frederic
|
r15691 | return False | ||
Jonathan Frederic
|
r15693 | if return_code: | ||
# Command error | ||||
return False | ||||
return check_version(out.lstrip('v'), '0.9.12') | ||||