1# -*- coding: utf-8 -*- 2 3''' 4Math extension for Python-Markdown 5================================== 6 7Adds support for displaying math formulas using 8[MathJax](http://www.mathjax.org/). 9 10Author: 2015-2020, Dmitry Shachnev <mitya57@gmail.com>. 11''' 12 13from xml.etree.ElementTree import Element 14from markdown.inlinepatterns import InlineProcessor 15from markdown.extensions import Extension 16from markdown.preprocessors import Preprocessor 17from markdown.util import AtomicString 18 19 20def _wrap_node(node, preview_text, wrapper_tag): 21 preview = Element('span', {'class': 'MathJax_Preview'}) 22 preview.text = AtomicString(preview_text) 23 wrapper = Element(wrapper_tag) 24 wrapper.extend([preview, node]) 25 return wrapper 26 27 28class InlineMathPattern(InlineProcessor): 29 def handleMatch(self, m, data): 30 node = Element('script') 31 node.set('type', self._content_type) 32 node.text = AtomicString(m.group(2)) 33 if self._add_preview: 34 node = _wrap_node(node, m.group(0), 'span') 35 return node, m.start(0), m.end(0) 36 37 38class DisplayMathPattern(InlineProcessor): 39 def handleMatch(self, m, data): 40 node = Element('script') 41 node.set('type', '%s; mode=display' % self._content_type) 42 if '\\begin' in m.group(1): 43 node.text = AtomicString(m.group(0)) 44 else: 45 node.text = AtomicString(m.group(2)) 46 if self._add_preview: 47 node = _wrap_node(node, m.group(0), 'div') 48 return node, m.start(0), m.end(0) 49 50 51class GitLabPreprocessor(Preprocessor): 52 """ 53 Preprocessor for GitLab-style standalone syntax: 54 55 ```math 56 math goes here 57 ``` 58 """ 59 60 def run(self, lines): 61 inside_math_block = False 62 math_block_start = None 63 math_blocks = [] 64 65 for line_number, line in enumerate(lines): 66 if line.strip() == '```math' and not inside_math_block: 67 math_block_start = line_number 68 inside_math_block = True 69 if line.strip() == '```' and inside_math_block: 70 math_blocks.append((math_block_start, line_number)) 71 inside_math_block = False 72 73 for math_block_start, math_block_end in reversed(math_blocks): 74 math_lines = lines[math_block_start + 1:math_block_end] 75 math_content = '\n'.join(math_lines) 76 html = '<script type="%s; mode=display">\n%s\n</script>\n' 77 html %= (self._content_type, math_content) 78 placeholder = self.md.htmlStash.store(html) 79 lines[math_block_start:math_block_end + 1] = [placeholder] 80 return lines 81 82 83class MathExtension(Extension): 84 def __init__(self, *args, **kwargs): 85 self.config = { 86 'enable_dollar_delimiter': 87 [False, 'Enable single-dollar delimiter'], 88 'add_preview': [False, 'Add a preview node before each math node'], 89 'use_asciimath': 90 [False, 'Use AsciiMath syntax instead of TeX syntax'], 91 'use_gitlab_delimiters': 92 [False, 'Use GitLab-style $`...`$ delimiters'], 93 } 94 super(MathExtension, self).__init__(*args, **kwargs) 95 96 def extendMarkdown(self, md): 97 add_preview = self.getConfig('add_preview') 98 use_asciimath = self.getConfig('use_asciimath') 99 use_gitlab_delimiters = self.getConfig('use_gitlab_delimiters') 100 content_type = 'math/asciimath' if use_asciimath else 'math/tex' 101 102 inlinemathpatterns = ( 103 InlineMathPattern(r'(?<!\\|\$)(\$)([^\$]+)(\$)'), # $...$ 104 InlineMathPattern(r'(?<!\\)(\\\()(.+?)(\\\))') # \(...\) 105 ) 106 mathpatterns = ( 107 DisplayMathPattern(r'(?<!\\)(\$\$)([^\$]+)(\$\$)'), # $$...$$ 108 DisplayMathPattern(r'(?<!\\)(\\\[)(.+?)(\\\])'), # \[...\] 109 DisplayMathPattern( # \begin...\end 110 r'(?<!\\)(\\begin{([a-z]+?\*?)})(.+?)(\\end{\2})') 111 ) 112 if not self.getConfig('enable_dollar_delimiter'): 113 inlinemathpatterns = inlinemathpatterns[1:] 114 if use_asciimath: 115 mathpatterns = mathpatterns[:-1] # \begin...\end is TeX only 116 if use_gitlab_delimiters: 117 # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#math 118 inlinemathpatterns = ( 119 InlineMathPattern(r'(?<!\\)(\$`)([^`]+)(`\$)'), # $`...`$ 120 ) 121 mathpatterns = () 122 preprocessor = GitLabPreprocessor(md) 123 preprocessor._content_type = content_type 124 # we should have higher priority than 'fenced_code_block' which 125 # has 25 126 md.preprocessors.register(preprocessor, 'math-gitlab', 27) 127 128 for i, pattern in enumerate(mathpatterns): 129 pattern._add_preview = add_preview 130 pattern._content_type = content_type 131 # we should have higher priority than 'escape' which has 180 132 md.inlinePatterns.register(pattern, 'math-%d' % i, 185) 133 for i, pattern in enumerate(inlinemathpatterns): 134 pattern._add_preview = add_preview 135 pattern._content_type = content_type 136 # to use gitlab delimiters, we should have higher priority than 137 # 'backtick' which has 190 138 priority = 195 if use_gitlab_delimiters else 185 139 md.inlinePatterns.register(pattern, 'math-inline-%d' % i, priority) 140 if self.getConfig('enable_dollar_delimiter'): 141 md.ESCAPED_CHARS.append('$') 142 143 144def makeExtension(*args, **kwargs): 145 return MathExtension(*args, **kwargs) 146