##// END OF EJS Templates
Fix mathjax pass-through with mistune
Thomas Kluyver -
Show More
@@ -1,214 +1,217
1 1 """Markdown filters
2
2 3 This file contains a collection of utility filters for dealing with
3 4 markdown within Jinja templates.
4 5 """
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
7 #
6 # Copyright (c) IPython Development Team.
8 7 # Distributed under the terms of the Modified BSD License.
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
12 8
13 #-----------------------------------------------------------------------------
14 # Imports
15 #-----------------------------------------------------------------------------
16 9 from __future__ import print_function
17 10
18 11 # Stdlib imports
19 12 import os
20 13 import subprocess
21 14 from io import TextIOWrapper, BytesIO
22 15 import re
23 16
24 17 import mistune
25 18 from pygments import highlight
26 19 from pygments.lexers import get_lexer_by_name
27 20 from pygments.formatters import HtmlFormatter
28 21 from pygments.util import ClassNotFound
29 22
30 23 # IPython imports
31 24 from IPython.nbconvert.utils.pandoc import pandoc
32 25 from IPython.nbconvert.utils.exceptions import ConversionException
33 26 from IPython.utils.process import get_output_error_code
34 27 from IPython.utils.py3compat import cast_bytes
35 28 from IPython.utils.version import check_version
36 29
37 #-----------------------------------------------------------------------------
38 # Functions
39 #-----------------------------------------------------------------------------
30
40 31 marked = os.path.join(os.path.dirname(__file__), "marked.js")
41 32 _node = None
42 33
43 34 __all__ = [
44 35 'markdown2html',
45 36 'markdown2html_pandoc',
46 37 'markdown2html_marked',
47 38 'markdown2html_mistune',
48 39 'markdown2latex',
49 40 'markdown2rst',
50 41 ]
51 42
52 43 class NodeJSMissing(ConversionException):
53 44 """Exception raised when node.js is missing."""
54 45 pass
55 46
56 47 def markdown2latex(source):
57 48 """Convert a markdown string to LaTeX via pandoc.
58 49
59 50 This function will raise an error if pandoc is not installed.
60 51 Any error messages generated by pandoc are printed to stderr.
61 52
62 53 Parameters
63 54 ----------
64 55 source : string
65 56 Input string, assumed to be valid markdown.
66 57
67 58 Returns
68 59 -------
69 60 out : string
70 61 Output as returned by pandoc.
71 62 """
72 63 return pandoc(source, 'markdown', 'latex')
73 64
74 65 class MathBlockGrammar(mistune.BlockGrammar):
75 block_math = re.compile("^\$\$(.*?)\$\$")
76 block_math2 = re.compile(r"^\\begin(.*?)\\end")
66 block_math = re.compile("^\$\$(.*?)\$\$", re.DOTALL)
67 latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}",
68 re.DOTALL)
77 69
78 70 class MathBlockLexer(mistune.BlockLexer):
79 default_features = ['block_math', 'block_math2'] + mistune.BlockLexer.default_features
71 default_features = ['block_math', 'latex_environment'] + mistune.BlockLexer.default_features
80 72
81 73 def __init__(self, rules=None, **kwargs):
82 74 if rules is None:
83 75 rules = MathBlockGrammar()
84 76 super(MathBlockLexer, self).__init__(rules, **kwargs)
85 77
86 78 def parse_block_math(self, m):
87 79 """Parse a $$math$$ block"""
88 80 self.tokens.append({
89 81 'type': 'block_math',
90 82 'text': m.group(1)
91 83 })
92 84
93 parse_block_math2 = parse_block_math
85 def parse_latex_environment(self, m):
86 self.tokens.append({
87 'type': 'latex_environment',
88 'name': m.group(1),
89 'text': m.group(2)
90 })
94 91
95 92 class MathInlineGrammar(mistune.InlineGrammar):
96 93 math = re.compile("^\$(.+?)\$")
97 94
98 95 class MathInlineLexer(mistune.InlineLexer):
99 96 default_features = ['math'] + mistune.InlineLexer.default_features
100 97
101 98 def __init__(self, renderer, rules=None, **kwargs):
102 99 if rules is None:
103 100 rules = MathInlineGrammar()
104 101 super(MathInlineLexer, self).__init__(renderer, rules, **kwargs)
105 102
106 103 def output_math(self, m):
107 self.renderer.inline_math(m.group(1))
104 return self.renderer.inline_math(m.group(1))
108 105
109 106 class MarkdownWithMath(mistune.Markdown):
110 107 def __init__(self, renderer, **kwargs):
111 108 if 'inline' not in kwargs:
112 109 kwargs['inline'] = MathInlineLexer(renderer, **kwargs)
113 110 if 'block' not in kwargs:
114 111 kwargs['block'] = MathBlockLexer(**kwargs)
115 112 super(MarkdownWithMath, self).__init__(renderer, **kwargs)
116 113
117 114 def parse_block_math(self):
118 115 return self.renderer.block_math(self.token['text'])
119 116
117 def parse_latex_environment(self):
118 return self.renderer.latex_environment(self.token['name'], self.token['text'])
119
120 120 class IPythonRenderer(mistune.Renderer):
121 121 def block_code(self, code, lang):
122 122 if lang:
123 123 try:
124 124 lexer = get_lexer_by_name(lang, stripall=True)
125 125 except ClassNotFound:
126 126 code = lang + '\n' + code
127 127 lang = None
128 128
129 129 if not lang:
130 130 return '\n<pre><code>%s</code></pre>\n' % \
131 131 mistune.escape(code)
132 132
133 133 formatter = HtmlFormatter()
134 134 return highlight(code, lexer, formatter)
135 135
136 136 # Pass math through unaltered - mathjax does the rendering in the browser
137 137 def block_math(self, text):
138 138 return '$$%s$$' % text
139 139
140 def latex_environment(self, name, text):
141 return r'\begin{%s}%s\end{%s}' % (name, text, name)
142
140 143 def inline_math(self, text):
141 144 return '$%s$' % text
142 145
143 146 def markdown2html_mistune(source):
144 147 """Convert a markdown string to HTML using mistune"""
145 148 return MarkdownWithMath(renderer=IPythonRenderer()).render(source)
146 149
147 150 def markdown2html_pandoc(source):
148 151 """Convert a markdown string to HTML via pandoc"""
149 152 return pandoc(source, 'markdown', 'html', extra_args=['--mathjax'])
150 153
151 154 def _find_nodejs():
152 155 global _node
153 156 if _node is None:
154 157 # prefer md2html via marked if node.js >= 0.9.12 is available
155 158 # node is called nodejs on debian, so try that first
156 159 _node = 'nodejs'
157 160 if not _verify_node(_node):
158 161 _node = 'node'
159 162 return _node
160 163
161 164 def markdown2html_marked(source, encoding='utf-8'):
162 165 """Convert a markdown string to HTML via marked"""
163 166 command = [_find_nodejs(), marked]
164 167 try:
165 168 p = subprocess.Popen(command,
166 169 stdin=subprocess.PIPE, stdout=subprocess.PIPE
167 170 )
168 171 except OSError as e:
169 172 raise NodeJSMissing(
170 173 "The command '%s' returned an error: %s.\n" % (" ".join(command), e) +
171 174 "Please check that Node.js is installed."
172 175 )
173 176 out, _ = p.communicate(cast_bytes(source, encoding))
174 177 out = TextIOWrapper(BytesIO(out), encoding, 'replace').read()
175 178 return out.rstrip('\n')
176 179
177 180 # The mistune renderer is the default, because it's simple to depend on it
178 181 markdown2html = markdown2html_mistune
179 182
180 183 def markdown2rst(source):
181 184 """Convert a markdown string to ReST via pandoc.
182 185
183 186 This function will raise an error if pandoc is not installed.
184 187 Any error messages generated by pandoc are printed to stderr.
185 188
186 189 Parameters
187 190 ----------
188 191 source : string
189 192 Input string, assumed to be valid markdown.
190 193
191 194 Returns
192 195 -------
193 196 out : string
194 197 Output as returned by pandoc.
195 198 """
196 199 return pandoc(source, 'markdown', 'rst')
197 200
198 201 def _verify_node(cmd):
199 202 """Verify that the node command exists and is at least the minimum supported
200 203 version of node.
201 204
202 205 Parameters
203 206 ----------
204 207 cmd : string
205 208 Node command to verify (i.e 'node')."""
206 209 try:
207 210 out, err, return_code = get_output_error_code([cmd, '--version'])
208 211 except OSError:
209 212 # Command not found
210 213 return False
211 214 if return_code:
212 215 # Command error
213 216 return False
214 217 return check_version(out.lstrip('v'), '0.9.12')
@@ -1,91 +1,88
1 """Tests for conversions from markdown to other formats"""
1 2
2 """
3 Module with tests for Markdown
4 """
5
6 #-----------------------------------------------------------------------------
7 # Copyright (c) 2013, the IPython Development Team.
8 #
3 # Copyright (c) IPython Development Team.
9 4 # Distributed under the terms of the Modified BSD License.
10 #
11 # The full license is in the file COPYING.txt, distributed with this software.
12 #-----------------------------------------------------------------------------
13
14 #-----------------------------------------------------------------------------
15 # Imports
16 #-----------------------------------------------------------------------------
17 5
18 6 from copy import copy
19 7
20 8 from IPython.utils.py3compat import string_types
21 9 from IPython.testing import decorators as dec
22 10
23 11 from ...tests.base import TestsBase
24 12 from ..markdown import markdown2latex, markdown2html, markdown2rst
25 13
26 14
27 #-----------------------------------------------------------------------------
28 # Class
29 #-----------------------------------------------------------------------------
30
31 15 class TestMarkdown(TestsBase):
32 16
33 17 tests = [
34 18 '*test',
35 19 '**test',
36 20 '*test*',
37 21 '_test_',
38 22 '__test__',
39 23 '__*test*__',
40 24 '**test**',
41 25 '#test',
42 26 '##test',
43 27 'test\n----',
44 28 'test [link](https://google.com/)']
45 29
46 30 tokens = [
47 31 '*test',
48 32 '**test',
49 33 'test',
50 34 'test',
51 35 'test',
52 36 'test',
53 37 'test',
54 38 'test',
55 39 'test',
56 40 'test',
57 41 ('test', 'https://google.com/')]
58 42
59 43
60 44 @dec.onlyif_cmds_exist('pandoc')
61 45 def test_markdown2latex(self):
62 46 """markdown2latex test"""
63 47 for index, test in enumerate(self.tests):
64 48 self._try_markdown(markdown2latex, test, self.tokens[index])
65 49
66 50 def test_markdown2html(self):
67 51 """markdown2html test"""
68 52 for index, test in enumerate(self.tests):
69 53 self._try_markdown(markdown2html, test, self.tokens[index])
70 54
55 def test_markdown2html_math(self):
56 # Mathematical expressions should be passed through unaltered
57 cases = [("\\begin{equation*}\n"
58 "\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\n"
59 "\\end{equation*}"),
60 ("$$\n"
61 "a = 1 *3* 5\n"
62 "$$"),
63 "$ a = 1 *3* 5 $",
64 ]
65 for case in cases:
66 self.assertIn(case, markdown2html(case))
67
71 68
72 69 @dec.onlyif_cmds_exist('pandoc')
73 70 def test_markdown2rst(self):
74 71 """markdown2rst test"""
75 72
76 73 #Modify token array for rst, escape asterik
77 74 tokens = copy(self.tokens)
78 75 tokens[0] = r'\*test'
79 76 tokens[1] = r'\*\*test'
80 77
81 78 for index, test in enumerate(self.tests):
82 79 self._try_markdown(markdown2rst, test, tokens[index])
83 80
84 81
85 82 def _try_markdown(self, method, test, tokens):
86 83 results = method(test)
87 84 if isinstance(tokens, string_types):
88 85 assert tokens in results
89 86 else:
90 87 for token in tokens:
91 88 assert token in results
General Comments 0
You need to be logged in to leave comments. Login now