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