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