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