Merge branch 'master' into amsmath-ext
diff --git a/markdown_it/extensions/amsmath/__init__.py b/markdown_it/extensions/amsmath/__init__.py
new file mode 100644
index 0000000..c09f50b
--- /dev/null
+++ b/markdown_it/extensions/amsmath/__init__.py
@@ -0,0 +1,111 @@
+"""An extension to capture amsmath latex environments."""
+import re
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_block import StateBlock
+from markdown_it.common.utils import escapeHtml
+
+# Taken from amsmath version 2.1
+# http://anorien.csc.warwick.ac.uk/mirrors/CTAN/macros/latex/required/amsmath/amsldoc.pdf
+ENVIRONMENTS = [
+    # 3.2 single equation with an automatically gen-erated number
+    "equation",
+    # 3.3 variation equation, used for equations that don’t fit on a single line
+    "multline",
+    # 3.5 a group of consecutive equations when there is no alignment desired among them
+    "gather",
+    # 3.6 Used for two or more equations when vertical alignment is desired
+    "align",
+    # allows the horizontal space between equationsto be explicitly specified.
+    "alignat",
+    # stretches the space betweenthe equation columns to the maximum possible width
+    "flalign",
+    # 4.1 The pmatrix, bmatrix, Bmatrix, vmatrix and Vmatrix have (respectively)
+    # (),[],{},||,and ‖‖ delimiters built in.
+    "matrix",
+    "pmatrix",
+    "bmatrix",
+    "Bmatrix",
+    "vmatrix",
+    "Vmatrix",
+    # eqnarray is another math environment, it is not part of amsmath,
+    # and note that it is better to use align or equation+split instead
+    "eqnarray",
+]
+# other "non-top-level" environments:
+
+# 3.4 the split environment is for single equations that are too long to fit on one line
+# and hence must be split into multiple lines,
+# it is intended for use only inside some other displayed equation structure,
+# usually an equation, align, or gather environment
+
+# 3.7 variants gathered, aligned,and alignedat are provided
+# whose total width is the actual width of the contents;
+# thus they can be used as a component in a containing expression
+
+RE_OPEN = re.compile(r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}")
+
+
+def amsmath_plugin(md: MarkdownIt):
+
+    md.block.ruler.before(
+        "blockquote",
+        "amsmath",
+        amsmath_block,
+        {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+    )
+    md.add_render_rule("amsmath", render_amsmath_block)
+
+
+def match_environment(string):
+    match_open = RE_OPEN.match(string)
+    if not match_open:
+        return None
+    environment = match_open.group(1)
+    numbered = match_open.group(2)
+    match_close = re.search(
+        r"\\end\{" + environment + numbered.replace("*", r"\*") + "\\}", string
+    )
+    if not match_close:
+        return None
+    return (environment, numbered, match_close.end())
+
+
+def amsmath_block(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+    # if it's indented more than 3 spaces, it should be a code block
+    if state.sCount[startLine] - state.blkIndent >= 4:
+        return False
+
+    begin = state.bMarks[startLine] + state.tShift[startLine]
+
+    outcome = match_environment(state.src[begin:])
+    if not outcome:
+        return False
+    environment, numbered, endpos = outcome
+    endpos += begin
+
+    line = startLine
+    while line < endLine:
+        if endpos >= state.bMarks[line] and endpos <= state.eMarks[line]:
+            # line for end of block math found ...
+            state.line = line + 1
+            break
+        line += 1
+
+    if not silent:
+        token = state.push("amsmath", "math", 0)
+        token.block = True
+        token.content = state.src[begin:endpos]
+        token.meta = {"environment": environment, "numbered": numbered}
+        token.map = [startLine, line]
+
+    return True
+
+
+def render_amsmath_block(self, tokens, idx, options, env):
+    token = tokens[idx]
+    return (
+        '<section class="amsmath">\n<eqn>\n'
+        f"{escapeHtml(token.content)}\n</eqn>\n</section>\n"
+    )
diff --git a/tests/test_amsmath/__init__.py b/tests/test_amsmath/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_amsmath/__init__.py
diff --git a/tests/test_amsmath/fixtures.md b/tests/test_amsmath/fixtures.md
new file mode 100644
index 0000000..b023e60
--- /dev/null
+++ b/tests/test_amsmath/fixtures.md
@@ -0,0 +1,217 @@
+equation environment:
+.
+\begin{equation}
+a = 1
+\end{equation}
+.
+<section class="amsmath">
+<eqn>
+\begin{equation}
+a = 1
+\end{equation}
+</eqn>
+</section>
+.
+
+equation* environment:
+.
+\begin{equation*}
+a = 1
+\end{equation*}
+.
+<section class="amsmath">
+<eqn>
+\begin{equation*}
+a = 1
+\end{equation*}
+</eqn>
+</section>
+.
+
+multline environment:
+.
+\begin{multline}
+a = 1
+\end{multline}
+.
+<section class="amsmath">
+<eqn>
+\begin{multline}
+a = 1
+\end{multline}
+</eqn>
+</section>
+.
+
+multline* environment:
+.
+\begin{multline*}
+a = 1
+\end{multline*}
+.
+<section class="amsmath">
+<eqn>
+\begin{multline*}
+a = 1
+\end{multline*}
+</eqn>
+</section>
+.
+
+gather environment:
+.
+\begin{gather}
+a = 1
+\end{gather}
+.
+<section class="amsmath">
+<eqn>
+\begin{gather}
+a = 1
+\end{gather}
+</eqn>
+</section>
+.
+
+gather* environment:
+.
+\begin{gather*}
+a = 1
+\end{gather*}
+.
+<section class="amsmath">
+<eqn>
+\begin{gather*}
+a = 1
+\end{gather*}
+</eqn>
+</section>
+.
+
+align environment:
+.
+\begin{align}
+a = 1
+\end{align}
+.
+<section class="amsmath">
+<eqn>
+\begin{align}
+a = 1
+\end{align}
+</eqn>
+</section>
+.
+
+align* environment:
+.
+\begin{align*}
+a = 1
+\end{align*}
+.
+<section class="amsmath">
+<eqn>
+\begin{align*}
+a = 1
+\end{align*}
+</eqn>
+</section>
+.
+
+alignat environment:
+.
+\begin{alignat}
+a = 1
+\end{alignat}
+.
+<section class="amsmath">
+<eqn>
+\begin{alignat}
+a = 1
+\end{alignat}
+</eqn>
+</section>
+.
+
+alignat* environment:
+.
+\begin{alignat*}
+a = 1
+\end{alignat*}
+.
+<section class="amsmath">
+<eqn>
+\begin{alignat*}
+a = 1
+\end{alignat*}
+</eqn>
+</section>
+.
+
+flalign environment:
+.
+\begin{flalign}
+a = 1
+\end{flalign}
+.
+<section class="amsmath">
+<eqn>
+\begin{flalign}
+a = 1
+\end{flalign}
+</eqn>
+</section>
+.
+
+flalign* environment:
+.
+\begin{flalign*}
+a = 1
+\end{flalign*}
+.
+<section class="amsmath">
+<eqn>
+\begin{flalign*}
+a = 1
+\end{flalign*}
+</eqn>
+</section>
+.
+
+equation environment, with before/after paragraphs:
+.
+before
+\begin{equation}
+a = 1
+\end{equation}
+after
+.
+<p>before</p>
+<section class="amsmath">
+<eqn>
+\begin{equation}
+a = 1
+\end{equation}
+</eqn>
+</section>
+<p>after</p>
+.
+
+equation environment, in list:
+.
+- \begin{equation}
+  a = 1
+  \end{equation}
+.
+<ul>
+<li>
+<section class="amsmath">
+<eqn>
+\begin{equation}
+  a = 1
+  \end{equation}
+</eqn>
+</section>
+</li>
+</ul>
+.
diff --git a/tests/test_amsmath/test_amsmath.py b/tests/test_amsmath/test_amsmath.py
new file mode 100644
index 0000000..e41f883
--- /dev/null
+++ b/tests/test_amsmath/test_amsmath.py
@@ -0,0 +1,38 @@
+from pathlib import Path
+from textwrap import dedent
+
+import pytest
+
+from markdown_it import MarkdownIt
+from markdown_it.extensions.amsmath import amsmath_plugin
+from markdown_it.utils import read_fixture_file
+
+FIXTURE_PATH = Path(__file__).parent
+
+
+def test_plugin_parse(data_regression):
+    md = MarkdownIt().use(amsmath_plugin)
+    tokens = md.parse(
+        dedent(
+            """\
+    a
+    \\begin{equation}
+    b=1
+    c=2
+    \\end{equation}
+    d
+    """
+        )
+    )
+    data_regression.check([t.as_dict() for t in tokens])
+
+
+@pytest.mark.parametrize(
+    "line,title,input,expected", read_fixture_file(FIXTURE_PATH.joinpath("fixtures.md"))
+)
+def test_fixtures(line, title, input, expected):
+    md = MarkdownIt("commonmark").use(amsmath_plugin)
+    md.options["xhtmlOut"] = False
+    text = md.render(input)
+    print(text)
+    assert text.rstrip() == expected.rstrip()
diff --git a/tests/test_amsmath/test_amsmath/test_plugin_parse.yml b/tests/test_amsmath/test_amsmath/test_plugin_parse.yml
new file mode 100644
index 0000000..4d6b949
--- /dev/null
+++ b/tests/test_amsmath/test_amsmath/test_plugin_parse.yml
@@ -0,0 +1,135 @@
+- attrs: null
+  block: true
+  children: null
+  content: ''
+  hidden: false
+  info: ''
+  level: 0
+  map:
+  - 0
+  - 1
+  markup: ''
+  meta: {}
+  nesting: 1
+  tag: p
+  type: paragraph_open
+- attrs: null
+  block: true
+  children:
+  - attrs: null
+    block: false
+    children: null
+    content: a
+    hidden: false
+    info: ''
+    level: 0
+    map: null
+    markup: ''
+    meta: {}
+    nesting: 0
+    tag: ''
+    type: text
+  content: a
+  hidden: false
+  info: ''
+  level: 1
+  map:
+  - 0
+  - 1
+  markup: ''
+  meta: {}
+  nesting: 0
+  tag: ''
+  type: inline
+- attrs: null
+  block: true
+  children: null
+  content: ''
+  hidden: false
+  info: ''
+  level: 0
+  map: null
+  markup: ''
+  meta: {}
+  nesting: -1
+  tag: p
+  type: paragraph_close
+- attrs: null
+  block: true
+  children: null
+  content: '\begin{equation}
+
+    b=1
+
+    c=2
+
+    \end{equation}'
+  hidden: false
+  info: ''
+  level: 0
+  map:
+  - 1
+  - 4
+  markup: ''
+  meta:
+    environment: equation
+    numbered: ''
+  nesting: 0
+  tag: math
+  type: amsmath
+- attrs: null
+  block: true
+  children: null
+  content: ''
+  hidden: false
+  info: ''
+  level: 0
+  map:
+  - 5
+  - 6
+  markup: ''
+  meta: {}
+  nesting: 1
+  tag: p
+  type: paragraph_open
+- attrs: null
+  block: true
+  children:
+  - attrs: null
+    block: false
+    children: null
+    content: d
+    hidden: false
+    info: ''
+    level: 0
+    map: null
+    markup: ''
+    meta: {}
+    nesting: 0
+    tag: ''
+    type: text
+  content: d
+  hidden: false
+  info: ''
+  level: 1
+  map:
+  - 5
+  - 6
+  markup: ''
+  meta: {}
+  nesting: 0
+  tag: ''
+  type: inline
+- attrs: null
+  block: true
+  children: null
+  content: ''
+  hidden: false
+  info: ''
+  level: 0
+  map: null
+  markup: ''
+  meta: {}
+  nesting: -1
+  tag: p
+  type: paragraph_close