summaryrefslogtreecommitdiffstats
path: root/mdit_py_plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:29:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:29:52 +0000
commitfcb2f10732db61d216e2105c8154486f66b3e3ff (patch)
treeefda929db4b1543eecc583e3b7d9c0bad4cd86a6 /mdit_py_plugins
parentInitial commit. (diff)
downloadmdit-py-plugins-fcb2f10732db61d216e2105c8154486f66b3e3ff.tar.xz
mdit-py-plugins-fcb2f10732db61d216e2105c8154486f66b3e3ff.zip
Adding upstream version 0.3.3.upstream/0.3.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mdit_py_plugins')
-rw-r--r--mdit_py_plugins/__init__.py1
-rw-r--r--mdit_py_plugins/admon/LICENSE24
-rw-r--r--mdit_py_plugins/admon/__init__.py1
-rw-r--r--mdit_py_plugins/admon/index.py172
-rw-r--r--mdit_py_plugins/admon/port.yaml4
-rw-r--r--mdit_py_plugins/amsmath/__init__.py126
-rw-r--r--mdit_py_plugins/anchors/__init__.py1
-rw-r--r--mdit_py_plugins/anchors/index.py129
-rw-r--r--mdit_py_plugins/attrs/__init__.py1
-rw-r--r--mdit_py_plugins/attrs/index.py123
-rw-r--r--mdit_py_plugins/attrs/parse.py265
-rw-r--r--mdit_py_plugins/colon_fence.py132
-rw-r--r--mdit_py_plugins/container/LICENSE22
-rw-r--r--mdit_py_plugins/container/README.md95
-rw-r--r--mdit_py_plugins/container/__init__.py1
-rw-r--r--mdit_py_plugins/container/index.py174
-rw-r--r--mdit_py_plugins/container/port.yaml5
-rw-r--r--mdit_py_plugins/deflist/LICENSE22
-rw-r--r--mdit_py_plugins/deflist/README.md38
-rw-r--r--mdit_py_plugins/deflist/__init__.py1
-rw-r--r--mdit_py_plugins/deflist/index.py253
-rw-r--r--mdit_py_plugins/deflist/port.yaml5
-rw-r--r--mdit_py_plugins/dollarmath/__init__.py1
-rw-r--r--mdit_py_plugins/dollarmath/index.py339
-rw-r--r--mdit_py_plugins/field_list/__init__.py208
-rw-r--r--mdit_py_plugins/footnote/LICENSE22
-rw-r--r--mdit_py_plugins/footnote/__init__.py1
-rw-r--r--mdit_py_plugins/footnote/index.py430
-rw-r--r--mdit_py_plugins/footnote/port.yaml4
-rw-r--r--mdit_py_plugins/front_matter/LICENSE22
-rw-r--r--mdit_py_plugins/front_matter/__init__.py1
-rw-r--r--mdit_py_plugins/front_matter/index.py138
-rw-r--r--mdit_py_plugins/front_matter/port.yaml4
-rw-r--r--mdit_py_plugins/myst_blocks/__init__.py1
-rw-r--r--mdit_py_plugins/myst_blocks/index.py153
-rw-r--r--mdit_py_plugins/myst_role/__init__.py1
-rw-r--r--mdit_py_plugins/myst_role/index.py65
-rw-r--r--mdit_py_plugins/py.typed1
-rw-r--r--mdit_py_plugins/substitution.py113
-rw-r--r--mdit_py_plugins/tasklists/__init__.py153
-rw-r--r--mdit_py_plugins/tasklists/port.yaml6
-rw-r--r--mdit_py_plugins/texmath/LICENSE21
-rw-r--r--mdit_py_plugins/texmath/README.md137
-rw-r--r--mdit_py_plugins/texmath/__init__.py1
-rw-r--r--mdit_py_plugins/texmath/index.py307
-rw-r--r--mdit_py_plugins/texmath/port.yaml7
-rw-r--r--mdit_py_plugins/wordcount/__init__.py58
47 files changed, 3789 insertions, 0 deletions
diff --git a/mdit_py_plugins/__init__.py b/mdit_py_plugins/__init__.py
new file mode 100644
index 0000000..e19434e
--- /dev/null
+++ b/mdit_py_plugins/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.3.3"
diff --git a/mdit_py_plugins/admon/LICENSE b/mdit_py_plugins/admon/LICENSE
new file mode 100644
index 0000000..eb4033e
--- /dev/null
+++ b/mdit_py_plugins/admon/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
+Copyright (c) 2018 jebbs
+Copyright (c) 2021- commenthol
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mdit_py_plugins/admon/__init__.py b/mdit_py_plugins/admon/__init__.py
new file mode 100644
index 0000000..49b1c15
--- /dev/null
+++ b/mdit_py_plugins/admon/__init__.py
@@ -0,0 +1 @@
+from .index import admon_plugin # noqa: F401
diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py
new file mode 100644
index 0000000..8ebbe8f
--- /dev/null
+++ b/mdit_py_plugins/admon/index.py
@@ -0,0 +1,172 @@
+# Process admonitions and pass to cb.
+
+import math
+from typing import Callable, Optional, Tuple
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_block import StateBlock
+
+
+def get_tag(params: str) -> Tuple[str, str]:
+ if not params.strip():
+ return "", ""
+
+ tag, *_title = params.strip().split(" ")
+ joined = " ".join(_title)
+
+ title = ""
+ if not joined:
+ title = tag.title()
+ elif joined != '""':
+ title = joined
+ return tag.lower(), title
+
+
+def validate(params: str) -> bool:
+ tag = params.strip().split(" ", 1)[-1] or ""
+ return bool(tag)
+
+
+MIN_MARKERS = 3
+MARKER_STR = "!"
+MARKER_CHAR = ord(MARKER_STR)
+MARKER_LEN = len(MARKER_STR)
+
+
+def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # Check out the first character quickly, which should filter out most of non-containers
+ if MARKER_CHAR != ord(state.src[start]):
+ return False
+
+ # Check out the rest of the marker string
+ pos = start + 1
+ while pos <= maximum and MARKER_STR[(pos - start) % MARKER_LEN] == state.src[pos]:
+ pos += 1
+
+ marker_count = math.floor((pos - start) / MARKER_LEN)
+ if marker_count < MIN_MARKERS:
+ return False
+ marker_pos = pos - ((pos - start) % MARKER_LEN)
+ params = state.src[marker_pos:maximum]
+ markup = state.src[start:marker_pos]
+
+ if not validate(params):
+ return False
+
+ # Since start is found, we can report success here in validation mode
+ if silent:
+ return True
+
+ old_parent = state.parentType
+ old_line_max = state.lineMax
+ old_indent = state.blkIndent
+
+ blk_start = pos
+ while blk_start < maximum and state.src[blk_start] == " ":
+ blk_start += 1
+
+ state.parentType = "admonition"
+ state.blkIndent += blk_start - start
+
+ was_empty = False
+
+ # Search for the end of the block
+ next_line = startLine
+ while True:
+ next_line += 1
+ if next_line >= endLine:
+ # unclosed block should be autoclosed by end of document.
+ # also block seems to be autoclosed by end of parent
+ break
+ pos = state.bMarks[next_line] + state.tShift[next_line]
+ maximum = state.eMarks[next_line]
+ is_empty = state.sCount[next_line] < state.blkIndent
+
+ # two consecutive empty lines autoclose the block
+ if is_empty and was_empty:
+ break
+ was_empty = is_empty
+
+ if pos < maximum and state.sCount[next_line] < state.blkIndent:
+ # non-empty line with negative indent should stop the block:
+ # - !!!
+ # test
+ break
+
+ # this will prevent lazy continuations from ever going past our end marker
+ state.lineMax = next_line
+
+ tag, title = get_tag(params)
+
+ token = state.push("admonition_open", "div", 1)
+ token.markup = markup
+ token.block = True
+ token.attrs = {"class": f"admonition {tag}"}
+ token.meta = {"tag": tag}
+ token.content = title
+ token.info = params
+ token.map = [startLine, next_line]
+
+ if title:
+ title_markup = f"{markup} {tag}"
+ token = state.push("admonition_title_open", "p", 1)
+ token.markup = title_markup
+ token.attrs = {"class": "admonition-title"}
+ token.map = [startLine, startLine + 1]
+
+ token = state.push("inline", "", 0)
+ token.content = title
+ token.map = [startLine, startLine + 1]
+ token.children = []
+
+ token = state.push("admonition_title_close", "p", -1)
+ token.markup = title_markup
+
+ state.md.block.tokenize(state, startLine + 1, next_line)
+
+ token = state.push("admonition_close", "div", -1)
+ token.markup = state.src[start:pos]
+ token.block = True
+
+ state.parentType = old_parent
+ state.lineMax = old_line_max
+ state.blkIndent = old_indent
+ state.line = next_line
+
+ return True
+
+
+def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None:
+ """Plugin to use
+ `python-markdown style admonitions
+ <https://python-markdown.github.io/extensions/admonition>`_.
+
+ .. code-block:: md
+
+ !!! note
+ *content*
+
+ Note, this is ported from
+ `markdown-it-admon
+ <https://github.com/commenthol/markdown-it-admon>`_.
+ """
+
+ def renderDefault(self, tokens, idx, _options, env):
+ return self.renderToken(tokens, idx, _options, env)
+
+ render = render or renderDefault
+
+ md.add_render_rule("admonition_open", render)
+ md.add_render_rule("admonition_close", render)
+ md.add_render_rule("admonition_title_open", render)
+ md.add_render_rule("admonition_title_close", render)
+
+ md.block.ruler.before(
+ "fence",
+ "admonition",
+ admonition,
+ {"alt": ["paragraph", "reference", "blockquote", "list"]},
+ )
diff --git a/mdit_py_plugins/admon/port.yaml b/mdit_py_plugins/admon/port.yaml
new file mode 100644
index 0000000..d2835bc
--- /dev/null
+++ b/mdit_py_plugins/admon/port.yaml
@@ -0,0 +1,4 @@
+- package: markdown-it-admon
+ commit: 9820ba89415c464a3cc18a780f222a0ceb3e18bd
+ date: Jul 3, 2021
+ version: 1.0.0
diff --git a/mdit_py_plugins/amsmath/__init__.py b/mdit_py_plugins/amsmath/__init__.py
new file mode 100644
index 0000000..0b367b5
--- /dev/null
+++ b/mdit_py_plugins/amsmath/__init__.py
@@ -0,0 +1,126 @@
+"""An extension to capture amsmath latex environments."""
+import re
+from typing import Callable, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import escapeHtml
+from markdown_it.rules_block import StateBlock
+
+# 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, *, renderer: Optional[Callable[[str], str]] = None):
+ """Parses TeX math equations, without any surrounding delimiters,
+ only for top-level `amsmath <https://ctan.org/pkg/amsmath>`__ environments:
+
+ .. code-block:: latex
+
+ \\begin{gather*}
+ a_1=b_1+c_1\\\\
+ a_2=b_2+c_2-d_2+e_2
+ \\end{gather*}
+
+ :param renderer: Function to render content, by default escapes HTML
+
+ """
+ md.block.ruler.before(
+ "blockquote",
+ "amsmath",
+ amsmath_block,
+ {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+ )
+
+ if renderer is None:
+ _renderer = lambda content: escapeHtml(content)
+ else:
+ _renderer = renderer
+
+ def render_amsmath_block(self, tokens, idx, options, env):
+ content = _renderer(str(tokens[idx].content))
+ return f'<div class="math amsmath">\n{content}\n</div>\n'
+
+ 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
diff --git a/mdit_py_plugins/anchors/__init__.py b/mdit_py_plugins/anchors/__init__.py
new file mode 100644
index 0000000..d9c4f05
--- /dev/null
+++ b/mdit_py_plugins/anchors/__init__.py
@@ -0,0 +1 @@
+from .index import anchors_plugin # noqa F401
diff --git a/mdit_py_plugins/anchors/index.py b/mdit_py_plugins/anchors/index.py
new file mode 100644
index 0000000..abbd48a
--- /dev/null
+++ b/mdit_py_plugins/anchors/index.py
@@ -0,0 +1,129 @@
+import re
+from typing import Callable, List, Optional, Set
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_core import StateCore
+from markdown_it.token import Token
+
+
+def anchors_plugin(
+ md: MarkdownIt,
+ min_level: int = 1,
+ max_level: int = 2,
+ slug_func: Optional[Callable[[str], str]] = None,
+ permalink: bool = False,
+ permalinkSymbol: str = "¶",
+ permalinkBefore: bool = False,
+ permalinkSpace: bool = True,
+):
+ """Plugin for adding header anchors, based on
+ `markdown-it-anchor <https://github.com/valeriangalliat/markdown-it-anchor>`__
+
+ .. code-block:: md
+
+ # Title String
+
+ renders as:
+
+ .. code-block:: html
+
+ <h1 id="title-string">Title String <a class="header-anchor" href="#title-string">¶</a></h1>
+
+ :param min_level: minimum header level to apply anchors
+ :param max_level: maximum header level to apply anchors
+ :param slug_func: function to convert title text to id slug.
+ :param permalink: Add a permalink next to the title
+ :param permalinkSymbol: the symbol to show
+ :param permalinkBefore: Add the permalink before the title, otherwise after
+ :param permalinkSpace: Add a space between the permalink and the title
+
+ Note, the default slug function aims to mimic the GitHub Markdown format, see:
+
+ - https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
+ - https://gist.github.com/asabaylus/3071099
+
+ """
+ selected_levels = list(range(min_level, max_level + 1))
+ md.core.ruler.push(
+ "anchor",
+ _make_anchors_func(
+ selected_levels,
+ slug_func or slugify,
+ permalink,
+ permalinkSymbol,
+ permalinkBefore,
+ permalinkSpace,
+ ),
+ )
+
+
+def _make_anchors_func(
+ selected_levels: List[int],
+ slug_func: Callable[[str], str],
+ permalink: bool,
+ permalinkSymbol: str,
+ permalinkBefore: bool,
+ permalinkSpace: bool,
+):
+ def _anchor_func(state: StateCore):
+ slugs: Set[str] = set()
+ for (idx, token) in enumerate(state.tokens):
+ if token.type != "heading_open":
+ continue
+ level = int(token.tag[1])
+ if level not in selected_levels:
+ continue
+ inline_token = state.tokens[idx + 1]
+ assert inline_token.children is not None
+ title = "".join(
+ child.content
+ for child in inline_token.children
+ if child.type in ["text", "code_inline"]
+ )
+ slug = unique_slug(slug_func(title), slugs)
+ token.attrSet("id", slug)
+
+ if permalink:
+ link_open = Token(
+ "link_open",
+ "a",
+ 1,
+ )
+ link_open.attrSet("class", "header-anchor")
+ link_open.attrSet("href", f"#{slug}")
+ link_tokens = [
+ link_open,
+ Token("html_block", "", 0, content=permalinkSymbol),
+ Token("link_close", "a", -1),
+ ]
+ if permalinkBefore:
+ inline_token.children = (
+ link_tokens
+ + (
+ [Token("text", "", 0, content=" ")]
+ if permalinkSpace
+ else []
+ )
+ + inline_token.children
+ )
+ else:
+ inline_token.children.extend(
+ ([Token("text", "", 0, content=" ")] if permalinkSpace else [])
+ + link_tokens
+ )
+
+ return _anchor_func
+
+
+def slugify(title: str):
+ return re.sub(r"[^\w\u4e00-\u9fff\- ]", "", title.strip().lower().replace(" ", "-"))
+
+
+def unique_slug(slug: str, slugs: set):
+ uniq = slug
+ i = 1
+ while uniq in slugs:
+ uniq = f"{slug}-{i}"
+ i += 1
+ slugs.add(uniq)
+ return uniq
diff --git a/mdit_py_plugins/attrs/__init__.py b/mdit_py_plugins/attrs/__init__.py
new file mode 100644
index 0000000..9359cf8
--- /dev/null
+++ b/mdit_py_plugins/attrs/__init__.py
@@ -0,0 +1 @@
+from .index import attrs_plugin # noqa: F401
diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py
new file mode 100644
index 0000000..1adea3f
--- /dev/null
+++ b/mdit_py_plugins/attrs/index.py
@@ -0,0 +1,123 @@
+from typing import List, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_inline import StateInline
+from markdown_it.token import Token
+
+from .parse import ParseError, parse
+
+
+def attrs_plugin(
+ md: MarkdownIt,
+ *,
+ after=("image", "code_inline", "link_close", "span_close"),
+ spans=False,
+ span_after="link",
+):
+ """Parse inline attributes that immediately follow certain inline elements::
+
+ ![alt](https://image.com){#id .a b=c}
+
+ This syntax is inspired by
+ `Djot spans
+ <https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes>`_.
+
+ Inside the curly braces, the following syntax is possible:
+
+ - `.foo` specifies foo as a class.
+ Multiple classes may be given in this way; they will be combined.
+ - `#foo` specifies foo as an identifier.
+ An element may have only one identifier;
+ if multiple identifiers are given, the last one is used.
+ - `key="value"` or `key=value` specifies a key-value attribute.
+ Quotes are not needed when the value consists entirely of
+ ASCII alphanumeric characters or `_` or `:` or `-`.
+ Backslash escapes may be used inside quoted values.
+ - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`).
+
+ Multiple attribute blocks are merged.
+
+ :param md: The MarkdownIt instance to modify.
+ :param after: The names of inline elements after which attributes may be specified.
+ This plugin does not support attributes after emphasis, strikethrough or text elements,
+ which all require post-parse processing.
+ :param spans: If True, also parse attributes after spans of text, encapsulated by `[]`.
+ Note Markdown link references take precedence over this syntax.
+ :param span_after: The name of an inline rule after which spans may be specified.
+ """
+
+ def _attr_rule(state: StateInline, silent: bool):
+ if state.pending or not state.tokens:
+ return False
+ token = state.tokens[-1]
+ if token.type not in after:
+ return False
+ try:
+ new_pos, attrs = parse(state.src[state.pos :])
+ except ParseError:
+ return False
+ token_index = _find_opening(state.tokens, len(state.tokens) - 1)
+ if token_index is None:
+ return False
+ state.pos += new_pos + 1
+ if not silent:
+ attr_token = state.tokens[token_index]
+ if "class" in attrs and "class" in token.attrs:
+ attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}"
+ attr_token.attrs.update(attrs)
+ return True
+
+ if spans:
+ md.inline.ruler.after(span_after, "span", _span_rule)
+ md.inline.ruler.push("attr", _attr_rule)
+
+
+def _find_opening(tokens: List[Token], index: int) -> Optional[int]:
+ """Find the opening token index, if the token is closing."""
+ if tokens[index].nesting != -1:
+ return index
+ level = 0
+ while index >= 0:
+ level += tokens[index].nesting
+ if level == 0:
+ return index
+ index -= 1
+ return None
+
+
+def _span_rule(state: StateInline, silent: bool):
+ if state.srcCharCode[state.pos] != 0x5B: # /* [ */
+ return False
+
+ maximum = state.posMax
+ labelStart = state.pos + 1
+ labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False)
+
+ # parser failed to find ']', so it's not a valid span
+ if labelEnd < 0:
+ return False
+
+ pos = labelEnd + 1
+
+ # check not at end of inline
+ if pos >= maximum:
+ return False
+
+ try:
+ new_pos, attrs = parse(state.src[pos:])
+ except ParseError:
+ return False
+
+ pos += new_pos + 1
+
+ if not silent:
+ state.pos = labelStart
+ state.posMax = labelEnd
+ token = state.push("span_open", "span", 1)
+ token.attrs = attrs
+ state.md.inline.tokenize(state)
+ token = state.push("span_close", "span", -1)
+
+ state.pos = pos
+ state.posMax = maximum
+ return True
diff --git a/mdit_py_plugins/attrs/parse.py b/mdit_py_plugins/attrs/parse.py
new file mode 100644
index 0000000..4a30353
--- /dev/null
+++ b/mdit_py_plugins/attrs/parse.py
@@ -0,0 +1,265 @@
+"""Parser for attributes::
+
+ attributes { id = "foo", class = "bar baz",
+ key1 = "val1", key2 = "val2" }
+
+Adapted from:
+https://github.com/jgm/djot/blob/fae7364b86bfce69bc6d5b5eede1f5196d845fd6/djot/attributes.lua#L1
+
+syntax:
+
+attributes <- '{' whitespace* attribute (whitespace attribute)* whitespace* '}'
+attribute <- identifier | class | keyval
+identifier <- '#' name
+class <- '.' name
+name <- (nonspace, nonpunctuation other than ':', '_', '-')+
+keyval <- key '=' val
+key <- (ASCII_ALPHANUM | ':' | '_' | '-')+
+val <- bareval | quotedval
+bareval <- (ASCII_ALPHANUM | ':' | '_' | '-')+
+quotedval <- '"' ([^"] | '\"') '"'
+"""
+from __future__ import annotations
+
+from enum import Enum
+import re
+from typing import Callable
+
+
+class State(Enum):
+ START = 0
+ SCANNING = 1
+ SCANNING_ID = 2
+ SCANNING_CLASS = 3
+ SCANNING_KEY = 4
+ SCANNING_VALUE = 5
+ SCANNING_BARE_VALUE = 6
+ SCANNING_QUOTED_VALUE = 7
+ SCANNING_COMMENT = 8
+ SCANNING_ESCAPED = 9
+ DONE = 10
+
+
+REGEX_SPACE = re.compile(r"\s")
+REGEX_SPACE_PUNCTUATION = re.compile(r"[\s!\"#$%&'()*+,./;<=>?@[\]^`{|}~]")
+REGEX_KEY_CHARACTERS = re.compile(r"[a-zA-Z\d_:-]")
+
+
+class TokenState:
+ def __init__(self):
+ self._tokens = []
+ self.start: int = 0
+
+ def set_start(self, start: int) -> None:
+ self.start = start
+
+ def append(self, start: int, end: int, ttype: str):
+ self._tokens.append((start, end, ttype))
+
+ def compile(self, string: str) -> dict[str, str]:
+ """compile the tokens into a dictionary"""
+ attributes = {}
+ classes = []
+ idx = 0
+ while idx < len(self._tokens):
+ start, end, ttype = self._tokens[idx]
+ if ttype == "id":
+ attributes["id"] = string[start:end]
+ elif ttype == "class":
+ classes.append(string[start:end])
+ elif ttype == "key":
+ key = string[start:end]
+ if idx + 1 < len(self._tokens):
+ start, end, ttype = self._tokens[idx + 1]
+ if ttype == "value":
+ if key == "class":
+ classes.append(string[start:end])
+ else:
+ attributes[key] = string[start:end]
+ idx += 1
+ idx += 1
+ if classes:
+ attributes["class"] = " ".join(classes)
+ return attributes
+
+ def __str__(self) -> str:
+ return str(self._tokens)
+
+ def __repr__(self) -> str:
+ return repr(self._tokens)
+
+
+class ParseError(Exception):
+ def __init__(self, msg: str, pos: int) -> None:
+ self.pos = pos
+ super().__init__(msg + f" at position {pos}")
+
+
+def parse(string: str) -> tuple[int, dict[str, str]]:
+ """Parse attributes from start of string.
+
+ :returns: (length of parsed string, dict of attributes)
+ """
+ pos = 0
+ state: State = State.START
+ tokens = TokenState()
+ while pos < len(string):
+ state = HANDLERS[state](string[pos], pos, tokens)
+ if state == State.DONE:
+ return pos, tokens.compile(string)
+ pos = pos + 1
+
+ return pos, tokens.compile(string)
+
+
+def handle_start(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == "{":
+ return State.SCANNING
+ raise ParseError("Attributes must start with '{'", pos)
+
+
+def handle_scanning(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == " " or char == "\t" or char == "\n" or char == "\r":
+ return State.SCANNING
+ if char == "}":
+ return State.DONE
+ if char == "#":
+ tokens.set_start(pos)
+ return State.SCANNING_ID
+ if char == "%":
+ tokens.set_start(pos)
+ return State.SCANNING_COMMENT
+ if char == ".":
+ tokens.set_start(pos)
+ return State.SCANNING_CLASS
+ if REGEX_KEY_CHARACTERS.fullmatch(char):
+ tokens.set_start(pos)
+ return State.SCANNING_KEY
+
+ raise ParseError(f"Unexpected character whilst scanning: {char}", pos)
+
+
+def handle_scanning_comment(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == "%":
+ return State.SCANNING
+
+ return State.SCANNING_COMMENT
+
+
+def handle_scanning_id(char: str, pos: int, tokens: TokenState) -> State:
+
+ if not REGEX_SPACE_PUNCTUATION.fullmatch(char):
+ return State.SCANNING_ID
+
+ if char == "}":
+ if (pos - 1) > tokens.start:
+ tokens.append(tokens.start + 1, pos, "id")
+ return State.DONE
+
+ if REGEX_SPACE.fullmatch(char):
+ if (pos - 1) > tokens.start:
+ tokens.append(tokens.start + 1, pos, "id")
+ return State.SCANNING
+
+ raise ParseError(f"Unexpected character whilst scanning id: {char}", pos)
+
+
+def handle_scanning_class(char: str, pos: int, tokens: TokenState) -> State:
+
+ if not REGEX_SPACE_PUNCTUATION.fullmatch(char):
+ return State.SCANNING_CLASS
+
+ if char == "}":
+ if (pos - 1) > tokens.start:
+ tokens.append(tokens.start + 1, pos, "class")
+ return State.DONE
+
+ if REGEX_SPACE.fullmatch(char):
+ if (pos - 1) > tokens.start:
+ tokens.append(tokens.start + 1, pos, "class")
+ return State.SCANNING
+
+ raise ParseError(f"Unexpected character whilst scanning class: {char}", pos)
+
+
+def handle_scanning_key(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == "=":
+ tokens.append(tokens.start, pos, "key")
+ return State.SCANNING_VALUE
+
+ if REGEX_KEY_CHARACTERS.fullmatch(char):
+ return State.SCANNING_KEY
+
+ raise ParseError(f"Unexpected character whilst scanning key: {char}", pos)
+
+
+def handle_scanning_value(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == '"':
+ tokens.set_start(pos)
+ return State.SCANNING_QUOTED_VALUE
+
+ if REGEX_KEY_CHARACTERS.fullmatch(char):
+ tokens.set_start(pos)
+ return State.SCANNING_BARE_VALUE
+
+ raise ParseError(f"Unexpected character whilst scanning value: {char}", pos)
+
+
+def handle_scanning_bare_value(char: str, pos: int, tokens: TokenState) -> State:
+
+ if REGEX_KEY_CHARACTERS.fullmatch(char):
+ return State.SCANNING_BARE_VALUE
+
+ if char == "}":
+ tokens.append(tokens.start, pos, "value")
+ return State.DONE
+
+ if REGEX_SPACE.fullmatch(char):
+ tokens.append(tokens.start, pos, "value")
+ return State.SCANNING
+
+ raise ParseError(f"Unexpected character whilst scanning bare value: {char}", pos)
+
+
+def handle_scanning_escaped(char: str, pos: int, tokens: TokenState) -> State:
+ return State.SCANNING_QUOTED_VALUE
+
+
+def handle_scanning_quoted_value(char: str, pos: int, tokens: TokenState) -> State:
+
+ if char == '"':
+ tokens.append(tokens.start + 1, pos, "value")
+ return State.SCANNING
+
+ if char == "\\":
+ return State.SCANNING_ESCAPED
+
+ if char == "{" or char == "}":
+ raise ParseError(
+ f"Unexpected character whilst scanning quoted value: {char}", pos
+ )
+
+ if char == "\n":
+ tokens.append(tokens.start + 1, pos, "value")
+ return State.SCANNING_QUOTED_VALUE
+
+ return State.SCANNING_QUOTED_VALUE
+
+
+HANDLERS: dict[State, Callable[[str, int, TokenState], State]] = {
+ State.START: handle_start,
+ State.SCANNING: handle_scanning,
+ State.SCANNING_COMMENT: handle_scanning_comment,
+ State.SCANNING_ID: handle_scanning_id,
+ State.SCANNING_CLASS: handle_scanning_class,
+ State.SCANNING_KEY: handle_scanning_key,
+ State.SCANNING_VALUE: handle_scanning_value,
+ State.SCANNING_BARE_VALUE: handle_scanning_bare_value,
+ State.SCANNING_QUOTED_VALUE: handle_scanning_quoted_value,
+ State.SCANNING_ESCAPED: handle_scanning_escaped,
+}
diff --git a/mdit_py_plugins/colon_fence.py b/mdit_py_plugins/colon_fence.py
new file mode 100644
index 0000000..e2356ed
--- /dev/null
+++ b/mdit_py_plugins/colon_fence.py
@@ -0,0 +1,132 @@
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import escapeHtml, unescapeAll
+from markdown_it.rules_block import StateBlock
+
+
+def colon_fence_plugin(md: MarkdownIt):
+ """This plugin directly mimics regular fences, but with `:` colons.
+
+ Example::
+
+ :::name
+ contained text
+ :::
+
+ """
+
+ md.block.ruler.before(
+ "fence",
+ "colon_fence",
+ _rule,
+ {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+ )
+ md.add_render_rule("colon_fence", _render)
+
+
+def _rule(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ haveEndMarker = False
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ if pos + 3 > maximum:
+ return False
+
+ marker = state.srcCharCode[pos]
+
+ # /* : */
+ if marker != 0x3A:
+ return False
+
+ # scan marker length
+ mem = pos
+ pos = state.skipChars(pos, marker)
+
+ length = pos - mem
+
+ if length < 3:
+ return False
+
+ markup = state.src[mem:pos]
+ params = state.src[pos:maximum]
+
+ # Since start is found, we can report success here in validation mode
+ if silent:
+ return True
+
+ # search end of block
+ nextLine = startLine
+
+ while True:
+ nextLine += 1
+ if nextLine >= endLine:
+ # unclosed block should be autoclosed by end of document.
+ # also block seems to be autoclosed by end of parent
+ break
+
+ pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if pos < maximum and state.sCount[nextLine] < state.blkIndent:
+ # non-empty line with negative indent should stop the list:
+ # - ```
+ # test
+ break
+
+ if state.srcCharCode[pos] != marker:
+ continue
+
+ if state.sCount[nextLine] - state.blkIndent >= 4:
+ # closing fence should be indented less than 4 spaces
+ continue
+
+ pos = state.skipChars(pos, marker)
+
+ # closing code fence must be at least as long as the opening one
+ if pos - mem < length:
+ continue
+
+ # make sure tail has spaces only
+ pos = state.skipSpaces(pos)
+
+ if pos < maximum:
+ continue
+
+ haveEndMarker = True
+ # found!
+ break
+
+ # If a fence has heading spaces, they should be removed from its inner block
+ length = state.sCount[startLine]
+
+ state.line = nextLine + (1 if haveEndMarker else 0)
+
+ token = state.push("colon_fence", "code", 0)
+ token.info = params
+ token.content = state.getLines(startLine + 1, nextLine, length, True)
+ token.markup = markup
+ token.map = [startLine, state.line]
+
+ return True
+
+
+def _render(self, tokens, idx, options, env):
+ token = tokens[idx]
+ info = unescapeAll(token.info).strip() if token.info else ""
+ content = escapeHtml(token.content)
+ block_name = ""
+
+ if info:
+ block_name = info.split()[0]
+
+ return (
+ "<pre><code"
+ + (f' class="block-{block_name}" ' if block_name else "")
+ + ">"
+ + content
+ + "</code></pre>\n"
+ )
diff --git a/mdit_py_plugins/container/LICENSE b/mdit_py_plugins/container/LICENSE
new file mode 100644
index 0000000..e6c3230
--- /dev/null
+++ b/mdit_py_plugins/container/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mdit_py_plugins/container/README.md b/mdit_py_plugins/container/README.md
new file mode 100644
index 0000000..03868d7
--- /dev/null
+++ b/mdit_py_plugins/container/README.md
@@ -0,0 +1,95 @@
+# markdown-it-container
+
+[![Build Status](https://img.shields.io/travis/markdown-it/markdown-it-container/master.svg?style=flat)](https://travis-ci.org/markdown-it/markdown-it-container)
+[![NPM version](https://img.shields.io/npm/v/markdown-it-container.svg?style=flat)](https://www.npmjs.org/package/markdown-it-container)
+[![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-container/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-container?branch=master)
+
+> Plugin for creating block-level custom containers for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser.
+
+__v2.+ requires `markdown-it` v5.+, see changelog.__
+
+With this plugin you can create block containers like:
+
+```
+::: warning
+*here be dragons*
+:::
+```
+
+.... and specify how they should be rendered. If no renderer defined, `<div>` with
+container name class will be created:
+
+```html
+<div class="warning">
+<em>here be dragons</em>
+</div>
+```
+
+Markup is the same as for [fenced code blocks](http://spec.commonmark.org/0.18/#fenced-code-blocks).
+Difference is, that marker use another character and content is rendered as markdown markup.
+
+
+## Installation
+
+node.js, browser:
+
+```bash
+$ npm install markdown-it-container --save
+$ bower install markdown-it-container --save
+```
+
+
+## API
+
+```js
+var md = require('markdown-it')()
+ .use(require('markdown-it-container'), name [, options]);
+```
+
+Params:
+
+- __name__ - container name (mandatory)
+- __options:__
+ - __validate__ - optional, function to validate tail after opening marker, should
+ return `true` on success.
+ - __render__ - optional, renderer function for opening/closing tokens.
+ - __marker__ - optional (`:`), character to use in delimiter.
+
+
+## Example
+
+```js
+var md = require('markdown-it')();
+
+md.use(require('markdown-it-container'), 'spoiler', {
+
+ validate: function(params) {
+ return params.trim().match(/^spoiler\s+(.*)$/);
+ },
+
+ render: function (tokens, idx) {
+ var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
+
+ if (tokens[idx].nesting === 1) {
+ // opening tag
+ return '<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n';
+
+ } else {
+ // closing tag
+ return '</details>\n';
+ }
+ }
+});
+
+console.log(md.render('::: spoiler click me\n*content*\n:::\n'));
+
+// Output:
+//
+// <details><summary>click me</summary>
+// <p><em>content</em></p>
+// </details>
+```
+
+## License
+
+[MIT](https://github.com/markdown-it/markdown-it-container/blob/master/LICENSE)
diff --git a/mdit_py_plugins/container/__init__.py b/mdit_py_plugins/container/__init__.py
new file mode 100644
index 0000000..7a89c81
--- /dev/null
+++ b/mdit_py_plugins/container/__init__.py
@@ -0,0 +1 @@
+from .index import container_plugin # noqa F401
diff --git a/mdit_py_plugins/container/index.py b/mdit_py_plugins/container/index.py
new file mode 100644
index 0000000..b6edd43
--- /dev/null
+++ b/mdit_py_plugins/container/index.py
@@ -0,0 +1,174 @@
+"""Process block-level custom containers."""
+from math import floor
+from typing import Callable, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import charCodeAt
+from markdown_it.rules_block import StateBlock
+
+
+def container_plugin(
+ md: MarkdownIt,
+ name: str,
+ marker: str = ":",
+ validate: Optional[Callable[[str, str], bool]] = None,
+ render=None,
+):
+ """Plugin ported from
+ `markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__.
+
+ It is a plugin for creating block-level custom containers:
+
+ .. code-block:: md
+
+ :::: name
+ ::: name
+ *markdown*
+ :::
+ ::::
+
+ :param name: the name of the container to parse
+ :param marker: the marker character to use
+ :param validate: func(marker, param) -> bool, default matches against the name
+ :param render: render func
+
+ """
+
+ def validateDefault(params: str, *args):
+ return params.strip().split(" ", 2)[0] == name
+
+ def renderDefault(self, tokens, idx, _options, env):
+ # add a class to the opening tag
+ if tokens[idx].nesting == 1:
+ tokens[idx].attrJoin("class", name)
+
+ return self.renderToken(tokens, idx, _options, env)
+
+ min_markers = 3
+ marker_str = marker
+ marker_char = charCodeAt(marker_str, 0)
+ marker_len = len(marker_str)
+ validate = validate or validateDefault
+ render = render or renderDefault
+
+ def container_func(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ auto_closed = False
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # Check out the first character quickly,
+ # this should filter out most of non-containers
+ if marker_char != state.srcCharCode[start]:
+ return False
+
+ # Check out the rest of the marker string
+ pos = start + 1
+ while pos <= maximum:
+ try:
+ character = state.src[pos]
+ except IndexError:
+ break
+ if marker_str[(pos - start) % marker_len] != character:
+ break
+ pos += 1
+
+ marker_count = floor((pos - start) / marker_len)
+ if marker_count < min_markers:
+ return False
+ pos -= (pos - start) % marker_len
+
+ markup = state.src[start:pos]
+ params = state.src[pos:maximum]
+ assert validate is not None
+ if not validate(params, markup):
+ return False
+
+ # Since start is found, we can report success here in validation mode
+ if silent:
+ return True
+
+ # Search for the end of the block
+ nextLine = startLine
+
+ while True:
+ nextLine += 1
+ if nextLine >= endLine:
+ # unclosed block should be autoclosed by end of document.
+ # also block seems to be autoclosed by end of parent
+ break
+
+ start = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if start < maximum and state.sCount[nextLine] < state.blkIndent:
+ # non-empty line with negative indent should stop the list:
+ # - ```
+ # test
+ break
+
+ if marker_char != state.srcCharCode[start]:
+ continue
+
+ if state.sCount[nextLine] - state.blkIndent >= 4:
+ # closing fence should be indented less than 4 spaces
+ continue
+
+ pos = start + 1
+ while pos <= maximum:
+ try:
+ character = state.src[pos]
+ except IndexError:
+ break
+ if marker_str[(pos - start) % marker_len] != character:
+ break
+ pos += 1
+
+ # closing code fence must be at least as long as the opening one
+ if floor((pos - start) / marker_len) < marker_count:
+ continue
+
+ # make sure tail has spaces only
+ pos -= (pos - start) % marker_len
+ pos = state.skipSpaces(pos)
+
+ if pos < maximum:
+ continue
+
+ # found!
+ auto_closed = True
+ break
+
+ old_parent = state.parentType
+ old_line_max = state.lineMax
+ state.parentType = "container"
+
+ # this will prevent lazy continuations from ever going past our end marker
+ state.lineMax = nextLine
+
+ token = state.push(f"container_{name}_open", "div", 1)
+ token.markup = markup
+ token.block = True
+ token.info = params
+ token.map = [startLine, nextLine]
+
+ state.md.block.tokenize(state, startLine + 1, nextLine)
+
+ token = state.push(f"container_{name}_close", "div", -1)
+ token.markup = state.src[start:pos]
+ token.block = True
+
+ state.parentType = old_parent
+ state.lineMax = old_line_max
+ state.line = nextLine + (1 if auto_closed else 0)
+
+ return True
+
+ md.block.ruler.before(
+ "fence",
+ "container_" + name,
+ container_func,
+ {"alt": ["paragraph", "reference", "blockquote", "list"]},
+ )
+ md.add_render_rule(f"container_{name}_open", render)
+ md.add_render_rule(f"container_{name}_close", render)
diff --git a/mdit_py_plugins/container/port.yaml b/mdit_py_plugins/container/port.yaml
new file mode 100644
index 0000000..e47c118
--- /dev/null
+++ b/mdit_py_plugins/container/port.yaml
@@ -0,0 +1,5 @@
+- package: markdown-it-container
+ commit: adb3defde3a1c56015895b47ce4c6591b8b1e3a2
+ date: Jun 2, 2020
+ version: 3.0.0
+ changes:
diff --git a/mdit_py_plugins/deflist/LICENSE b/mdit_py_plugins/deflist/LICENSE
new file mode 100644
index 0000000..2fd4e3d
--- /dev/null
+++ b/mdit_py_plugins/deflist/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mdit_py_plugins/deflist/README.md b/mdit_py_plugins/deflist/README.md
new file mode 100644
index 0000000..414157b
--- /dev/null
+++ b/mdit_py_plugins/deflist/README.md
@@ -0,0 +1,38 @@
+# markdown-it-deflist
+
+[![Build Status](https://img.shields.io/travis/markdown-it/markdown-it-deflist/master.svg?style=flat)](https://travis-ci.org/markdown-it/markdown-it-deflist)
+[![NPM version](https://img.shields.io/npm/v/markdown-it-deflist.svg?style=flat)](https://www.npmjs.org/package/markdown-it-deflist)
+[![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-deflist/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-deflist?branch=master)
+
+> Definition list (`<dl>`) tag plugin for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser.
+
+__v2.+ requires `markdown-it` v5.+, see changelog.__
+
+Syntax is based on [pandoc definition lists](http://johnmacfarlane.net/pandoc/README.html#definition-lists).
+
+
+## Install
+
+node.js, browser:
+
+```bash
+npm install markdown-it-deflist --save
+bower install markdown-it-deflist --save
+```
+
+## Use
+
+```js
+var md = require('markdown-it')()
+ .use(require('markdown-it-deflist'));
+
+md.render(/*...*/);
+```
+
+_Differences in browser._ If you load script directly into the page, without
+package system, module will add itself globally as `window.markdownitDeflist`.
+
+
+## License
+
+[MIT](https://github.com/markdown-it/markdown-it-deflist/blob/master/LICENSE)
diff --git a/mdit_py_plugins/deflist/__init__.py b/mdit_py_plugins/deflist/__init__.py
new file mode 100644
index 0000000..fbd9b0e
--- /dev/null
+++ b/mdit_py_plugins/deflist/__init__.py
@@ -0,0 +1 @@
+from .index import deflist_plugin # noqa F401
diff --git a/mdit_py_plugins/deflist/index.py b/mdit_py_plugins/deflist/index.py
new file mode 100644
index 0000000..0b353db
--- /dev/null
+++ b/mdit_py_plugins/deflist/index.py
@@ -0,0 +1,253 @@
+"""Process definition lists."""
+from markdown_it import MarkdownIt
+from markdown_it.rules_block import StateBlock
+
+
+def deflist_plugin(md: MarkdownIt):
+ """Plugin ported from
+ `markdown-it-deflist <https://github.com/markdown-it/markdown-it-deflist>`__.
+
+ The syntax is based on
+ `pandoc definition lists <http://johnmacfarlane.net/pandoc/README.html#definition-lists>`__:
+
+ .. code-block:: md
+
+ Term 1
+ : Definition 1 long form
+
+ second paragraph
+
+ Term 2 with *inline markup*
+ ~ Definition 2a compact style
+ ~ Definition 2b
+
+ """
+ isSpace = md.utils.isSpace # type: ignore
+
+ def skipMarker(state: StateBlock, line: int):
+ """Search `[:~][\n ]`, returns next pos after marker on success or -1 on fail."""
+ start = state.bMarks[line] + state.tShift[line]
+ maximum = state.eMarks[line]
+
+ if start >= maximum:
+ return -1
+
+ # Check bullet
+ marker = state.srcCharCode[start]
+ start += 1
+ if marker != 0x7E and marker != 0x3A: # ~ :
+ return -1
+
+ pos = state.skipSpaces(start)
+
+ # require space after ":"
+ if start == pos:
+ return -1
+
+ # no empty definitions, e.g. " : "
+ if pos >= maximum:
+ return -1
+
+ return start
+
+ def markTightParagraphs(state: StateBlock, idx: int):
+
+ level = state.level + 2
+
+ i = idx + 2
+ l2 = len(state.tokens) - 2
+ while i < l2:
+ if (
+ state.tokens[i].level == level
+ and state.tokens[i].type == "paragraph_open"
+ ):
+ state.tokens[i + 2].hidden = True
+ state.tokens[i].hidden = True
+ i += 2
+ i += 1
+
+ def deflist(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ if silent:
+ # quirk: validation mode validates a dd block only, not a whole deflist
+ if state.ddIndent < 0:
+ return False
+ return skipMarker(state, startLine) >= 0
+
+ nextLine = startLine + 1
+ if nextLine >= endLine:
+ return False
+
+ if state.isEmpty(nextLine):
+ nextLine += 1
+ if nextLine >= endLine:
+ return False
+
+ if state.sCount[nextLine] < state.blkIndent:
+ return False
+ contentStart = skipMarker(state, nextLine)
+ if contentStart < 0:
+ return False
+
+ # Start list
+ listTokIdx = len(state.tokens)
+ tight = True
+
+ token = state.push("dl_open", "dl", 1)
+ token.map = listLines = [startLine, 0]
+
+ # Iterate list items
+ dtLine = startLine
+ ddLine = nextLine
+
+ # One definition list can contain multiple DTs,
+ # and one DT can be followed by multiple DDs.
+ #
+ # Thus, there is two loops here, and label is
+ # needed to break out of the second one
+ #
+ break_outer = False
+
+ while True:
+ prevEmptyEnd = False
+
+ token = state.push("dt_open", "dt", 1)
+ token.map = [dtLine, dtLine]
+
+ token = state.push("inline", "", 0)
+ token.map = [dtLine, dtLine]
+ token.content = state.getLines(
+ dtLine, dtLine + 1, state.blkIndent, False
+ ).strip()
+ token.children = []
+
+ token = state.push("dt_close", "dt", -1)
+
+ while True:
+ token = state.push("dd_open", "dd", 1)
+ token.map = itemLines = [nextLine, 0]
+
+ pos = contentStart
+ maximum = state.eMarks[ddLine]
+ offset = (
+ state.sCount[ddLine]
+ + contentStart
+ - (state.bMarks[ddLine] + state.tShift[ddLine])
+ )
+
+ while pos < maximum:
+ ch = state.srcCharCode[pos]
+
+ if isSpace(ch):
+ if ch == 0x09:
+ offset += 4 - offset % 4
+ else:
+ offset += 1
+ else:
+ break
+
+ pos += 1
+
+ contentStart = pos
+
+ oldTight = state.tight
+ oldDDIndent = state.ddIndent
+ oldIndent = state.blkIndent
+ oldTShift = state.tShift[ddLine]
+ oldSCount = state.sCount[ddLine]
+ oldParentType = state.parentType
+ state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
+ state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
+ state.sCount[ddLine] = offset
+ state.tight = True
+ state.parentType = "deflist"
+
+ state.md.block.tokenize(state, ddLine, endLine, True)
+
+ # If any of list item is tight, mark list as tight
+ if not state.tight or prevEmptyEnd:
+ tight = False
+
+ # Item become loose if finish with empty line,
+ # but we should filter last element, because it means list finish
+ prevEmptyEnd = (state.line - ddLine) > 1 and state.isEmpty(
+ state.line - 1
+ )
+
+ state.tShift[ddLine] = oldTShift
+ state.sCount[ddLine] = oldSCount
+ state.tight = oldTight
+ state.parentType = oldParentType
+ state.blkIndent = oldIndent
+ state.ddIndent = oldDDIndent
+
+ token = state.push("dd_close", "dd", -1)
+
+ itemLines[1] = nextLine = state.line
+
+ if nextLine >= endLine:
+ break_outer = True
+ break
+
+ if state.sCount[nextLine] < state.blkIndent:
+ break_outer = True
+ break
+
+ contentStart = skipMarker(state, nextLine)
+ if contentStart < 0:
+ break
+
+ ddLine = nextLine
+
+ # go to the next loop iteration:
+ # insert DD tag and repeat checking
+
+ if break_outer:
+ break_outer = False
+ break
+
+ if nextLine >= endLine:
+ break
+ dtLine = nextLine
+
+ if state.isEmpty(dtLine):
+ break
+ if state.sCount[dtLine] < state.blkIndent:
+ break
+
+ ddLine = dtLine + 1
+ if ddLine >= endLine:
+ break
+ if state.isEmpty(ddLine):
+ ddLine += 1
+ if ddLine >= endLine:
+ break
+
+ if state.sCount[ddLine] < state.blkIndent:
+ break
+ contentStart = skipMarker(state, ddLine)
+ if contentStart < 0:
+ break
+
+ # go to the next loop iteration:
+ # insert DT and DD tags and repeat checking
+
+ # Finalise list
+ token = state.push("dl_close", "dl", -1)
+
+ listLines[1] = nextLine
+
+ state.line = nextLine
+
+ # mark paragraphs tight if needed
+ if tight:
+ markTightParagraphs(state, listTokIdx)
+
+ return True
+
+ md.block.ruler.before(
+ "paragraph",
+ "deflist",
+ deflist,
+ {"alt": ["paragraph", "reference", "blockquote"]},
+ )
diff --git a/mdit_py_plugins/deflist/port.yaml b/mdit_py_plugins/deflist/port.yaml
new file mode 100644
index 0000000..203c772
--- /dev/null
+++ b/mdit_py_plugins/deflist/port.yaml
@@ -0,0 +1,5 @@
+- package: markdown-it-deflist
+ commit: 20db400948520308291da029a23b0751cb30f3a0
+ date: July 12, 2017
+ version: 2.0.3
+ changes:
diff --git a/mdit_py_plugins/dollarmath/__init__.py b/mdit_py_plugins/dollarmath/__init__.py
new file mode 100644
index 0000000..1840543
--- /dev/null
+++ b/mdit_py_plugins/dollarmath/__init__.py
@@ -0,0 +1 @@
+from .index import dollarmath_plugin # noqa F401
diff --git a/mdit_py_plugins/dollarmath/index.py b/mdit_py_plugins/dollarmath/index.py
new file mode 100644
index 0000000..5fe0381
--- /dev/null
+++ b/mdit_py_plugins/dollarmath/index.py
@@ -0,0 +1,339 @@
+import re
+from typing import Any, Callable, Dict, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import escapeHtml, isWhiteSpace
+from markdown_it.rules_block import StateBlock
+from markdown_it.rules_inline import StateInline
+
+
+def dollarmath_plugin(
+ md: MarkdownIt,
+ *,
+ allow_labels: bool = True,
+ allow_space: bool = True,
+ allow_digits: bool = True,
+ double_inline: bool = False,
+ label_normalizer: Optional[Callable[[str], str]] = None,
+ renderer: Optional[Callable[[str, Dict[str, Any]], str]] = None,
+ label_renderer: Optional[Callable[[str], str]] = None,
+) -> None:
+ """Plugin for parsing dollar enclosed math,
+ e.g. inline: ``$a=1$``, block: ``$$b=2$$``
+
+ This is an improved version of ``texmath``; it is more performant,
+ and handles ``\\`` escaping properly and allows for more configuration.
+
+ :param allow_labels: Capture math blocks with label suffix, e.g. ``$$a=1$$ (eq1)``
+ :param allow_space: Parse inline math when there is space
+ after/before the opening/closing ``$``, e.g. ``$ a $``
+ :param allow_digits: Parse inline math when there is a digit
+ before/after the opening/closing ``$``, e.g. ``1$`` or ``$2``.
+ This is useful when also using currency.
+ :param double_inline: Search for double-dollar math within inline contexts
+ :param label_normalizer: Function to normalize the label,
+ by default replaces whitespace with `-`
+ :param renderer: Function to render content: `(str, {"display_mode": bool}) -> str`,
+ by default escapes HTML
+ :param label_renderer: Function to render labels, by default creates anchor
+
+ """
+ if label_normalizer is None:
+ label_normalizer = lambda label: re.sub(r"\s+", "-", label)
+
+ md.inline.ruler.before(
+ "escape",
+ "math_inline",
+ math_inline_dollar(allow_space, allow_digits, double_inline),
+ )
+ md.block.ruler.before(
+ "fence", "math_block", math_block_dollar(allow_labels, label_normalizer)
+ )
+
+ # TODO the current render rules are really just for testing
+ # would be good to allow "proper" math rendering,
+ # e.g. https://github.com/roniemartinez/latex2mathml
+
+ if renderer is None:
+ _renderer = lambda content, _: escapeHtml(content)
+ else:
+ _renderer = renderer
+
+ if label_renderer is None:
+ _label_renderer = (
+ lambda label: f'<a href="#{label}" class="mathlabel" title="Permalink to this equation">¶</a>' # noqa: E501
+ )
+ else:
+ _label_renderer = label_renderer
+
+ def render_math_inline(self, tokens, idx, options, env) -> str:
+ content = _renderer(str(tokens[idx].content).strip(), {"display_mode": False})
+ return f'<span class="math inline">{content}</span>'
+
+ def render_math_inline_double(self, tokens, idx, options, env) -> str:
+ content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True})
+ return f'<div class="math inline">{content}</div>'
+
+ def render_math_block(self, tokens, idx, options, env) -> str:
+ content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True})
+ return f'<div class="math block">\n{content}\n</div>\n'
+
+ def render_math_block_label(self, tokens, idx, options, env) -> str:
+ content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True})
+ _id = tokens[idx].info
+ label = _label_renderer(tokens[idx].info)
+ return f'<div id="{_id}" class="math block">\n{label}\n{content}\n</div>\n'
+
+ md.add_render_rule("math_inline", render_math_inline)
+ md.add_render_rule("math_inline_double", render_math_inline_double)
+
+ md.add_render_rule("math_block", render_math_block)
+ md.add_render_rule("math_block_label", render_math_block_label)
+
+
+def is_escaped(state: StateInline, back_pos: int, mod: int = 0) -> bool:
+ """Test if dollar is escaped."""
+ # count how many \ are before the current position
+ backslashes = 0
+ while back_pos >= 0:
+ back_pos = back_pos - 1
+ if state.srcCharCode[back_pos] == 0x5C: # /* \ */
+ backslashes += 1
+ else:
+ break
+
+ if not backslashes:
+ return False
+
+ # if an odd number of \ then ignore
+ if (backslashes % 2) != mod:
+ return True
+
+ return False
+
+
+def math_inline_dollar(
+ allow_space: bool = True, allow_digits: bool = True, allow_double: bool = False
+) -> Callable[[StateInline, bool], bool]:
+ """Generate inline dollar rule.
+
+ :param allow_space: Parse inline math when there is space
+ after/before the opening/closing ``$``, e.g. ``$ a $``
+ :param allow_digits: Parse inline math when there is a digit
+ before/after the opening/closing ``$``, e.g. ``1$`` or ``$2``.
+ This is useful when also using currency.
+ :param allow_double: Search for double-dollar math within inline contexts
+
+ """
+
+ def _math_inline_dollar(state: StateInline, silent: bool) -> bool:
+ """Inline dollar rule.
+
+ - Initial check:
+ - check if first character is a $
+ - check if the first character is escaped
+ - check if the next character is a space (if not allow_space)
+ - check if the next character is a digit (if not allow_digits)
+ - Advance one, if allow_double
+ - Find closing (advance one, if allow_double)
+ - Check closing:
+ - check if the previous character is a space (if not allow_space)
+ - check if the next character is a digit (if not allow_digits)
+ - Check empty content
+ """
+
+ # TODO options:
+ # even/odd backslash escaping
+
+ if state.srcCharCode[state.pos] != 0x24: # /* $ */
+ return False
+
+ if not allow_space:
+ # whitespace not allowed straight after opening $
+ try:
+ if isWhiteSpace(state.srcCharCode[state.pos + 1]):
+ return False
+ except IndexError:
+ return False
+
+ if not allow_digits:
+ # digit not allowed straight before opening $
+ try:
+ if state.src[state.pos - 1].isdigit():
+ return False
+ except IndexError:
+ pass
+
+ if is_escaped(state, state.pos):
+ return False
+
+ try:
+ is_double = allow_double and state.srcCharCode[state.pos + 1] == 0x24
+ except IndexError:
+ return False
+
+ # find closing $
+ pos = state.pos + 1 + (1 if is_double else 0)
+ found_closing = False
+ while not found_closing:
+ try:
+ end = state.srcCharCode.index(0x24, pos)
+ except ValueError:
+ return False
+
+ if is_escaped(state, end):
+ pos = end + 1
+ continue
+
+ try:
+ if is_double and not state.srcCharCode[end + 1] == 0x24:
+ pos = end + 1
+ continue
+ except IndexError:
+ return False
+
+ if is_double:
+ end += 1
+
+ found_closing = True
+
+ if not found_closing:
+ return False
+
+ if not allow_space:
+ # whitespace not allowed straight before closing $
+ try:
+ if isWhiteSpace(state.srcCharCode[end - 1]):
+ return False
+ except IndexError:
+ return False
+
+ if not allow_digits:
+ # digit not allowed straight after closing $
+ try:
+ if state.src[end + 1].isdigit():
+ return False
+ except IndexError:
+ pass
+
+ text = (
+ state.src[state.pos + 2 : end - 1]
+ if is_double
+ else state.src[state.pos + 1 : end]
+ )
+
+ # ignore empty
+ if not text:
+ return False
+
+ if not silent:
+ token = state.push(
+ "math_inline_double" if is_double else "math_inline", "math", 0
+ )
+ token.content = text
+ token.markup = "$$" if is_double else "$"
+
+ state.pos = end + 1
+
+ return True
+
+ return _math_inline_dollar
+
+
+# reversed end of block dollar equation, with equation label
+DOLLAR_EQNO_REV = re.compile(r"^\s*\)([^)$\r\n]+?)\(\s*\${2}")
+
+
+def math_block_dollar(
+ allow_labels: bool = True,
+ label_normalizer: Optional[Callable[[str], str]] = None,
+) -> Callable[[StateBlock, int, int, bool], bool]:
+ """Generate block dollar rule."""
+
+ def _math_block_dollar(
+ state: StateBlock, startLine: int, endLine: int, silent: bool
+ ) -> bool:
+
+ # TODO internal backslash escaping
+
+ haveEndMarker = False
+ startPos = state.bMarks[startLine] + state.tShift[startLine]
+ end = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ if startPos + 2 > end:
+ return False
+
+ if (
+ state.srcCharCode[startPos] != 0x24
+ or state.srcCharCode[startPos + 1] != 0x24
+ ): # /* $ */
+ return False
+
+ # search for end of block
+ nextLine = startLine
+ label = None
+
+ # search for end of block on same line
+ lineText = state.src[startPos:end]
+ if len(lineText.strip()) > 3:
+
+ if lineText.strip().endswith("$$"):
+ haveEndMarker = True
+ end = end - 2 - (len(lineText) - len(lineText.strip()))
+ elif allow_labels:
+ # reverse the line and match
+ eqnoMatch = DOLLAR_EQNO_REV.match(lineText[::-1])
+ if eqnoMatch:
+ haveEndMarker = True
+ label = eqnoMatch.group(1)[::-1]
+ end = end - eqnoMatch.end()
+
+ # search for end of block on subsequent line
+ if not haveEndMarker:
+ while True:
+ nextLine += 1
+ if nextLine >= endLine:
+ break
+
+ start = state.bMarks[nextLine] + state.tShift[nextLine]
+ end = state.eMarks[nextLine]
+
+ if end - start < 2:
+ continue
+
+ lineText = state.src[start:end]
+
+ if lineText.strip().endswith("$$"):
+ haveEndMarker = True
+ end = end - 2 - (len(lineText) - len(lineText.strip()))
+ break
+
+ # reverse the line and match
+ if allow_labels:
+ eqnoMatch = DOLLAR_EQNO_REV.match(lineText[::-1])
+ if eqnoMatch:
+ haveEndMarker = True
+ label = eqnoMatch.group(1)[::-1]
+ end = end - eqnoMatch.end()
+ break
+
+ if not haveEndMarker:
+ return False
+
+ state.line = nextLine + (1 if haveEndMarker else 0)
+
+ token = state.push("math_block_label" if label else "math_block", "math", 0)
+ token.block = True
+ token.content = state.src[startPos + 2 : end]
+ token.markup = "$$"
+ token.map = [startLine, state.line]
+ if label:
+ token.info = label if label_normalizer is None else label_normalizer(label)
+
+ return True
+
+ return _math_block_dollar
diff --git a/mdit_py_plugins/field_list/__init__.py b/mdit_py_plugins/field_list/__init__.py
new file mode 100644
index 0000000..9e21fb5
--- /dev/null
+++ b/mdit_py_plugins/field_list/__init__.py
@@ -0,0 +1,208 @@
+"""Field list plugin"""
+from contextlib import contextmanager
+from typing import Tuple
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_block import StateBlock
+
+
+def fieldlist_plugin(md: MarkdownIt):
+ """Field lists are mappings from field names to field bodies, based on the
+ `reStructureText syntax
+ <https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#field-lists>`_.
+
+ .. code-block:: md
+
+ :name *markup*:
+ :name1: body content
+ :name2: paragraph 1
+
+ paragraph 2
+ :name3:
+ paragraph 1
+
+ paragraph 2
+
+ A field name may consist of any characters except colons (":").
+ Inline markup is parsed in field names.
+
+ The field name is followed by whitespace and the field body.
+ The field body may be empty or contain multiple body elements.
+ The field body is aligned either by the start of the body on the first line or,
+ if no body content is on the first line, by 2 spaces.
+ """
+ md.block.ruler.before(
+ "paragraph",
+ "fieldlist",
+ _fieldlist_rule,
+ {"alt": ["paragraph", "reference", "blockquote"]},
+ )
+
+
+def parseNameMarker(state: StateBlock, startLine: int) -> Tuple[int, str]:
+ """Parse field name: `:name:`
+
+ :returns: position after name marker, name text
+ """
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ pos = start
+ maximum = state.eMarks[startLine]
+
+ # marker should have at least 3 chars (colon + character + colon)
+ if pos + 2 >= maximum:
+ return -1, ""
+
+ # first character should be ':'
+ if state.src[pos] != ":":
+ return -1, ""
+
+ # scan name length
+ name_length = 1
+ found_close = False
+ for ch in state.src[pos + 1 :]:
+ if ch == "\n":
+ break
+ if ch == ":":
+ # TODO backslash escapes
+ found_close = True
+ break
+ name_length += 1
+
+ if not found_close:
+ return -1, ""
+
+ # get name
+ name_text = state.src[pos + 1 : pos + name_length]
+
+ # name should contain at least one character
+ if not name_text.strip():
+ return -1, ""
+
+ return pos + name_length + 1, name_text
+
+
+@contextmanager
+def set_parent_type(state: StateBlock, name: str):
+ """Temporarily set parent type to `name`"""
+ oldParentType = state.parentType
+ state.parentType = name
+ yield
+ state.parentType = oldParentType
+
+
+def _fieldlist_rule(state: StateBlock, startLine: int, endLine: int, silent: bool):
+ # adapted from markdown_it/rules_block/list.py::list_block
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ posAfterName, name_text = parseNameMarker(state, startLine)
+ if posAfterName < 0:
+ return False
+
+ # For validation mode we can terminate immediately
+ if silent:
+ return True
+
+ # start field list
+ token = state.push("field_list_open", "dl", 1)
+ token.attrSet("class", "field-list")
+ token.map = listLines = [startLine, 0]
+
+ # iterate list items
+ nextLine = startLine
+
+ with set_parent_type(state, "fieldlist"):
+
+ while nextLine < endLine:
+
+ # create name tokens
+ token = state.push("fieldlist_name_open", "dt", 1)
+ token.map = [startLine, startLine]
+ token = state.push("inline", "", 0)
+ token.map = [startLine, startLine]
+ token.content = name_text
+ token.children = []
+ token = state.push("fieldlist_name_close", "dt", -1)
+
+ # set indent positions
+ pos = posAfterName
+ maximum = state.eMarks[nextLine]
+ offset = (
+ state.sCount[nextLine]
+ + posAfterName
+ - (state.bMarks[startLine] + state.tShift[startLine])
+ )
+
+ # find indent to start of body on first line
+ while pos < maximum:
+ ch = state.srcCharCode[pos]
+
+ if ch == 0x09: # \t
+ offset += 4 - (offset + state.bsCount[nextLine]) % 4
+ elif ch == 0x20: # \s
+ offset += 1
+ else:
+ break
+
+ pos += 1
+
+ contentStart = pos
+
+ # set indent for body text
+ if contentStart >= maximum:
+ # no body on first line, so use constant indentation
+ # TODO adapt to indentation of subsequent lines?
+ indent = 2
+ else:
+ indent = offset
+
+ # Run subparser on the field body
+ token = state.push("fieldlist_body_open", "dd", 1)
+ token.map = itemLines = [startLine, 0]
+
+ # change current state, then restore it after parser subcall
+ oldTShift = state.tShift[startLine]
+ oldSCount = state.sCount[startLine]
+ oldBlkIndent = state.blkIndent
+
+ state.tShift[startLine] = contentStart - state.bMarks[startLine]
+ state.sCount[startLine] = offset
+ state.blkIndent = indent
+
+ state.md.block.tokenize(state, startLine, endLine)
+
+ state.blkIndent = oldBlkIndent
+ state.tShift[startLine] = oldTShift
+ state.sCount[startLine] = oldSCount
+
+ token = state.push("fieldlist_body_close", "dd", -1)
+
+ nextLine = startLine = state.line
+ itemLines[1] = nextLine
+
+ if nextLine >= endLine:
+ break
+
+ contentStart = state.bMarks[startLine]
+
+ # Try to check if list is terminated or continued.
+ if state.sCount[nextLine] < state.blkIndent:
+ break
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ break
+
+ # get next field item
+ posAfterName, name_text = parseNameMarker(state, startLine)
+ if posAfterName < 0:
+ break
+
+ # Finalize list
+ token = state.push("field_list_close", "dl", -1)
+ listLines[1] = nextLine
+ state.line = nextLine
+
+ return True
diff --git a/mdit_py_plugins/footnote/LICENSE b/mdit_py_plugins/footnote/LICENSE
new file mode 100644
index 0000000..2fd4e3d
--- /dev/null
+++ b/mdit_py_plugins/footnote/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mdit_py_plugins/footnote/__init__.py b/mdit_py_plugins/footnote/__init__.py
new file mode 100644
index 0000000..d85daae
--- /dev/null
+++ b/mdit_py_plugins/footnote/__init__.py
@@ -0,0 +1 @@
+from .index import footnote_plugin # noqa: F401
diff --git a/mdit_py_plugins/footnote/index.py b/mdit_py_plugins/footnote/index.py
new file mode 100644
index 0000000..119fb71
--- /dev/null
+++ b/mdit_py_plugins/footnote/index.py
@@ -0,0 +1,430 @@
+# Process footnotes
+#
+
+from typing import List, Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import isSpace
+from markdown_it.helpers import parseLinkLabel
+from markdown_it.rules_block import StateBlock
+from markdown_it.rules_inline import StateInline
+from markdown_it.token import Token
+
+
+def footnote_plugin(md: MarkdownIt):
+ """Plugin ported from
+ `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.
+
+ It is based on the
+ `pandoc definition <http://johnmacfarlane.net/pandoc/README.html#footnotes>`__:
+
+ .. code-block:: md
+
+ Normal footnote:
+
+ Here is a footnote reference,[^1] and another.[^longnote]
+
+ [^1]: Here is the footnote.
+
+ [^longnote]: Here's one with multiple blocks.
+
+ Subsequent paragraphs are indented to show that they
+ belong to the previous footnote.
+
+ """
+ md.block.ruler.before(
+ "reference", "footnote_def", footnote_def, {"alt": ["paragraph", "reference"]}
+ )
+ md.inline.ruler.after("image", "footnote_inline", footnote_inline)
+ md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref)
+ md.core.ruler.after("inline", "footnote_tail", footnote_tail)
+
+ md.add_render_rule("footnote_ref", render_footnote_ref)
+ md.add_render_rule("footnote_block_open", render_footnote_block_open)
+ md.add_render_rule("footnote_block_close", render_footnote_block_close)
+ md.add_render_rule("footnote_open", render_footnote_open)
+ md.add_render_rule("footnote_close", render_footnote_close)
+ md.add_render_rule("footnote_anchor", render_footnote_anchor)
+
+ # helpers (only used in other rules, no tokens are attached to those)
+ md.add_render_rule("footnote_caption", render_footnote_caption)
+ md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)
+
+
+# ## RULES ##
+
+
+def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool):
+ """Process footnote block definition"""
+
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # line should be at least 5 chars - "[^x]:"
+ if start + 4 > maximum:
+ return False
+
+ if state.srcCharCode[start] != 0x5B: # /* [ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5E: # /* ^ */
+ return False
+
+ pos = start + 2
+ while pos < maximum:
+ if state.srcCharCode[pos] == 0x20:
+ return False
+ if state.srcCharCode[pos] == 0x5D: # /* ] */
+ break
+ pos += 1
+
+ if pos == start + 2: # no empty footnote labels
+ return False
+ pos += 1
+ if pos >= maximum or state.srcCharCode[pos] != 0x3A: # /* : */
+ return False
+ if silent:
+ return True
+ pos += 1
+
+ label = state.src[start + 2 : pos - 2]
+ state.env.setdefault("footnotes", {}).setdefault("refs", {})[":" + label] = -1
+
+ open_token = Token("footnote_reference_open", "", 1)
+ open_token.meta = {"label": label}
+ open_token.level = state.level
+ state.level += 1
+ state.tokens.append(open_token)
+
+ oldBMark = state.bMarks[startLine]
+ oldTShift = state.tShift[startLine]
+ oldSCount = state.sCount[startLine]
+ oldParentType = state.parentType
+
+ posAfterColon = pos
+ initial = offset = (
+ state.sCount[startLine]
+ + pos
+ - (state.bMarks[startLine] + state.tShift[startLine])
+ )
+
+ while pos < maximum:
+ ch = state.srcCharCode[pos]
+
+ if isSpace(ch):
+ if ch == 0x09:
+ offset += 4 - offset % 4
+ else:
+ offset += 1
+
+ else:
+ break
+
+ pos += 1
+
+ state.tShift[startLine] = pos - posAfterColon
+ state.sCount[startLine] = offset - initial
+
+ state.bMarks[startLine] = posAfterColon
+ state.blkIndent += 4
+ state.parentType = "footnote"
+
+ if state.sCount[startLine] < state.blkIndent:
+ state.sCount[startLine] += state.blkIndent
+
+ state.md.block.tokenize(state, startLine, endLine, True)
+
+ state.parentType = oldParentType
+ state.blkIndent -= 4
+ state.tShift[startLine] = oldTShift
+ state.sCount[startLine] = oldSCount
+ state.bMarks[startLine] = oldBMark
+
+ open_token.map = [startLine, state.line]
+
+ token = Token("footnote_reference_close", "", -1)
+ state.level -= 1
+ token.level = state.level
+ state.tokens.append(token)
+
+ return True
+
+
+def footnote_inline(state: StateInline, silent: bool):
+ """Process inline footnotes (^[...])"""
+
+ maximum = state.posMax
+ start = state.pos
+
+ if start + 2 >= maximum:
+ return False
+ if state.srcCharCode[start] != 0x5E: # /* ^ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5B: # /* [ */
+ return False
+
+ labelStart = start + 2
+ labelEnd = parseLinkLabel(state, start + 1)
+
+ # parser failed to find ']', so it's not a valid note
+ if labelEnd < 0:
+ return False
+
+ # We found the end of the link, and know for a fact it's a valid link
+ # so all that's left to do is to call tokenizer.
+ #
+ if not silent:
+ refs = state.env.setdefault("footnotes", {}).setdefault("list", {})
+ footnoteId = len(refs)
+
+ tokens: List[Token] = []
+ state.md.inline.parse(
+ state.src[labelStart:labelEnd], state.md, state.env, tokens
+ )
+
+ token = state.push("footnote_ref", "", 0)
+ token.meta = {"id": footnoteId}
+
+ refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens}
+
+ state.pos = labelEnd + 1
+ state.posMax = maximum
+ return True
+
+
+def footnote_ref(state: StateInline, silent: bool):
+ """Process footnote references ([^...])"""
+
+ maximum = state.posMax
+ start = state.pos
+
+ # should be at least 4 chars - "[^x]"
+ if start + 3 > maximum:
+ return False
+
+ if "footnotes" not in state.env or "refs" not in state.env["footnotes"]:
+ return False
+ if state.srcCharCode[start] != 0x5B: # /* [ */
+ return False
+ if state.srcCharCode[start + 1] != 0x5E: # /* ^ */
+ return False
+
+ pos = start + 2
+ while pos < maximum:
+ if state.srcCharCode[pos] == 0x20:
+ return False
+ if state.srcCharCode[pos] == 0x0A:
+ return False
+ if state.srcCharCode[pos] == 0x5D: # /* ] */
+ break
+ pos += 1
+
+ if pos == start + 2: # no empty footnote labels
+ return False
+ if pos >= maximum:
+ return False
+ pos += 1
+
+ label = state.src[start + 2 : pos - 1]
+ if (":" + label) not in state.env["footnotes"]["refs"]:
+ return False
+
+ if not silent:
+ if "list" not in state.env["footnotes"]:
+ state.env["footnotes"]["list"] = {}
+
+ if state.env["footnotes"]["refs"][":" + label] < 0:
+ footnoteId = len(state.env["footnotes"]["list"])
+ state.env["footnotes"]["list"][footnoteId] = {"label": label, "count": 0}
+ state.env["footnotes"]["refs"][":" + label] = footnoteId
+ else:
+ footnoteId = state.env["footnotes"]["refs"][":" + label]
+
+ footnoteSubId = state.env["footnotes"]["list"][footnoteId]["count"]
+ state.env["footnotes"]["list"][footnoteId]["count"] += 1
+
+ token = state.push("footnote_ref", "", 0)
+ token.meta = {"id": footnoteId, "subId": footnoteSubId, "label": label}
+
+ state.pos = pos
+ state.posMax = maximum
+ return True
+
+
+def footnote_tail(state: StateBlock, *args, **kwargs):
+ """Post-processing step, to move footnote tokens to end of the token stream.
+
+ Also removes un-referenced tokens.
+ """
+
+ insideRef = False
+ refTokens = {}
+
+ if "footnotes" not in state.env:
+ return
+
+ current: List[Token] = []
+ tok_filter = []
+ for tok in state.tokens:
+
+ if tok.type == "footnote_reference_open":
+ insideRef = True
+ current = []
+ currentLabel = tok.meta["label"]
+ tok_filter.append(False)
+ continue
+
+ if tok.type == "footnote_reference_close":
+ insideRef = False
+ # prepend ':' to avoid conflict with Object.prototype members
+ refTokens[":" + currentLabel] = current
+ tok_filter.append(False)
+ continue
+
+ if insideRef:
+ current.append(tok)
+
+ tok_filter.append((not insideRef))
+
+ state.tokens = [t for t, f in zip(state.tokens, tok_filter) if f]
+
+ if "list" not in state.env.get("footnotes", {}):
+ return
+ foot_list = state.env["footnotes"]["list"]
+
+ token = Token("footnote_block_open", "", 1)
+ state.tokens.append(token)
+
+ for i, foot_note in foot_list.items():
+ token = Token("footnote_open", "", 1)
+ token.meta = {"id": i, "label": foot_note.get("label", None)}
+ # TODO propagate line positions of original foot note
+ # (but don't store in token.map, because this is used for scroll syncing)
+ state.tokens.append(token)
+
+ if "tokens" in foot_note:
+ tokens = []
+
+ token = Token("paragraph_open", "p", 1)
+ token.block = True
+ tokens.append(token)
+
+ token = Token("inline", "", 0)
+ token.children = foot_note["tokens"]
+ token.content = foot_note["content"]
+ tokens.append(token)
+
+ token = Token("paragraph_close", "p", -1)
+ token.block = True
+ tokens.append(token)
+
+ elif "label" in foot_note:
+ tokens = refTokens[":" + foot_note["label"]]
+
+ state.tokens.extend(tokens)
+ if state.tokens[len(state.tokens) - 1].type == "paragraph_close":
+ lastParagraph: Optional[Token] = state.tokens.pop()
+ else:
+ lastParagraph = None
+
+ t = (
+ foot_note["count"]
+ if (("count" in foot_note) and (foot_note["count"] > 0))
+ else 1
+ )
+ j = 0
+ while j < t:
+ token = Token("footnote_anchor", "", 0)
+ token.meta = {"id": i, "subId": j, "label": foot_note.get("label", None)}
+ state.tokens.append(token)
+ j += 1
+
+ if lastParagraph:
+ state.tokens.append(lastParagraph)
+
+ token = Token("footnote_close", "", -1)
+ state.tokens.append(token)
+
+ token = Token("footnote_block_close", "", -1)
+ state.tokens.append(token)
+
+
+########################################
+# Renderer partials
+
+
+def render_footnote_anchor_name(self, tokens, idx, options, env):
+ n = str(tokens[idx].meta["id"] + 1)
+ prefix = ""
+
+ doc_id = env.get("docId", None)
+ if isinstance(doc_id, str):
+ prefix = f"-{doc_id}-"
+
+ return prefix + n
+
+
+def render_footnote_caption(self, tokens, idx, options, env):
+ n = str(tokens[idx].meta["id"] + 1)
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ n += ":" + str(tokens[idx].meta["subId"])
+
+ return "[" + n + "]"
+
+
+def render_footnote_ref(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+ caption = self.rules["footnote_caption"](tokens, idx, options, env)
+ refid = ident
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ refid += ":" + str(tokens[idx].meta["subId"])
+
+ return (
+ '<sup class="footnote-ref"><a href="#fn'
+ + ident
+ + '" id="fnref'
+ + refid
+ + '">'
+ + caption
+ + "</a></sup>"
+ )
+
+
+def render_footnote_block_open(self, tokens, idx, options, env):
+ return (
+ (
+ '<hr class="footnotes-sep" />\n'
+ if options.xhtmlOut
+ else '<hr class="footnotes-sep">\n'
+ )
+ + '<section class="footnotes">\n'
+ + '<ol class="footnotes-list">\n'
+ )
+
+
+def render_footnote_block_close(self, tokens, idx, options, env):
+ return "</ol>\n</section>\n"
+
+
+def render_footnote_open(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+
+ if tokens[idx].meta.get("subId", -1) > 0:
+ ident += ":" + tokens[idx].meta["subId"]
+
+ return '<li id="fn' + ident + '" class="footnote-item">'
+
+
+def render_footnote_close(self, tokens, idx, options, env):
+ return "</li>\n"
+
+
+def render_footnote_anchor(self, tokens, idx, options, env):
+ ident = self.rules["footnote_anchor_name"](tokens, idx, options, env)
+
+ if tokens[idx].meta["subId"] > 0:
+ ident += ":" + str(tokens[idx].meta["subId"])
+
+ # ↩ with escape code to prevent display as Apple Emoji on iOS
+ return ' <a href="#fnref' + ident + '" class="footnote-backref">\u21a9\uFE0E</a>'
diff --git a/mdit_py_plugins/footnote/port.yaml b/mdit_py_plugins/footnote/port.yaml
new file mode 100644
index 0000000..722f5e4
--- /dev/null
+++ b/mdit_py_plugins/footnote/port.yaml
@@ -0,0 +1,4 @@
+- package: markdown-it-footnote
+ commit: cab6665ba39c6eb517cbbae3baeb549004bf740c
+ date: Jul 9, 2019
+ version: 3.0.2
diff --git a/mdit_py_plugins/front_matter/LICENSE b/mdit_py_plugins/front_matter/LICENSE
new file mode 100644
index 0000000..54c0b84
--- /dev/null
+++ b/mdit_py_plugins/front_matter/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2016-2020 ParkSB.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/mdit_py_plugins/front_matter/__init__.py b/mdit_py_plugins/front_matter/__init__.py
new file mode 100644
index 0000000..26bce1d
--- /dev/null
+++ b/mdit_py_plugins/front_matter/__init__.py
@@ -0,0 +1 @@
+from .index import front_matter_plugin # noqa: F401
diff --git a/mdit_py_plugins/front_matter/index.py b/mdit_py_plugins/front_matter/index.py
new file mode 100644
index 0000000..2077925
--- /dev/null
+++ b/mdit_py_plugins/front_matter/index.py
@@ -0,0 +1,138 @@
+# Process front matter and pass to cb
+from math import floor
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import charCodeAt
+from markdown_it.rules_block import StateBlock
+
+
+def front_matter_plugin(md: MarkdownIt):
+ """Plugin ported from
+ `markdown-it-front-matter <https://github.com/ParkSB/markdown-it-front-matter>`__.
+
+ It parses initial metadata, stored between opening/closing dashes:
+
+ .. code-block:: md
+
+ ---
+ valid-front-matter: true
+ ---
+
+ """
+ frontMatter = make_front_matter_rule()
+ md.block.ruler.before(
+ "table",
+ "front_matter",
+ frontMatter,
+ {"alt": ["paragraph", "reference", "blockquote", "list"]},
+ )
+
+
+def make_front_matter_rule():
+ min_markers = 3
+ marker_str = "-"
+ marker_char = charCodeAt(marker_str, 0)
+ marker_len = len(marker_str)
+
+ def frontMatter(state: StateBlock, startLine: int, endLine: int, silent: bool):
+ auto_closed = False
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+ src_len = len(state.src)
+
+ # Check out the first character of the first line quickly,
+ # this should filter out non-front matter
+ if startLine != 0 or marker_char != state.srcCharCode[0]:
+ return False
+
+ # Check out the rest of the marker string
+ # while pos <= 3
+ pos = start + 1
+ while pos <= maximum and pos < src_len:
+ if marker_str[(pos - start) % marker_len] != state.src[pos]:
+ break
+ pos += 1
+
+ marker_count = floor((pos - start) / marker_len)
+
+ if marker_count < min_markers:
+ return False
+
+ pos -= (pos - start) % marker_len
+
+ # Since start is found, we can report success here in validation mode
+ if silent:
+ return True
+
+ # Search for the end of the block
+ nextLine = startLine
+
+ while True:
+ nextLine += 1
+ if nextLine >= endLine:
+ # unclosed block should be autoclosed by end of document.
+ return False
+
+ if state.src[start:maximum] == "...":
+ break
+
+ start = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if start < maximum and state.sCount[nextLine] < state.blkIndent:
+ # non-empty line with negative indent should stop the list:
+ # - ```
+ # test
+ break
+
+ if marker_char != state.srcCharCode[start]:
+ continue
+
+ if state.sCount[nextLine] - state.blkIndent >= 4:
+ # closing fence should be indented less than 4 spaces
+ continue
+
+ pos = start + 1
+ while pos < maximum:
+ if marker_str[(pos - start) % marker_len] != state.src[pos]:
+ break
+ pos += 1
+
+ # closing code fence must be at least as long as the opening one
+ if floor((pos - start) / marker_len) < marker_count:
+ continue
+
+ # make sure tail has spaces only
+ pos -= (pos - start) % marker_len
+ pos = state.skipSpaces(pos)
+
+ if pos < maximum:
+ continue
+
+ # found!
+ auto_closed = True
+ break
+
+ old_parent = state.parentType
+ old_line_max = state.lineMax
+ state.parentType = "container"
+
+ # this will prevent lazy continuations from ever going past our end marker
+ state.lineMax = nextLine
+
+ token = state.push("front_matter", "", 0)
+ token.hidden = True
+ token.markup = marker_str * min_markers
+ token.content = state.src[
+ state.bMarks[startLine + 1] : state.eMarks[nextLine - 1]
+ ]
+ token.block = True
+
+ state.parentType = old_parent
+ state.lineMax = old_line_max
+ state.line = nextLine + (1 if auto_closed else 0)
+ token.map = [startLine, state.line]
+
+ return True
+
+ return frontMatter
diff --git a/mdit_py_plugins/front_matter/port.yaml b/mdit_py_plugins/front_matter/port.yaml
new file mode 100644
index 0000000..f7d145f
--- /dev/null
+++ b/mdit_py_plugins/front_matter/port.yaml
@@ -0,0 +1,4 @@
+- package: markdown-it-front-matter
+ commit: b404f5d8fd536e7e9ddb276267ae0b6f76e9cf9d
+ date: Feb 7, 2020
+ version: 0.2.1
diff --git a/mdit_py_plugins/myst_blocks/__init__.py b/mdit_py_plugins/myst_blocks/__init__.py
new file mode 100644
index 0000000..434cad6
--- /dev/null
+++ b/mdit_py_plugins/myst_blocks/__init__.py
@@ -0,0 +1 @@
+from .index import myst_block_plugin # noqa: F401
diff --git a/mdit_py_plugins/myst_blocks/index.py b/mdit_py_plugins/myst_blocks/index.py
new file mode 100644
index 0000000..d0e4cf6
--- /dev/null
+++ b/mdit_py_plugins/myst_blocks/index.py
@@ -0,0 +1,153 @@
+import itertools
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import escapeHtml, isSpace
+from markdown_it.rules_block import StateBlock
+
+
+def myst_block_plugin(md: MarkdownIt):
+ """Parse MyST targets (``(name)=``), blockquotes (``% comment``) and block breaks (``+++``)."""
+ md.block.ruler.before(
+ "blockquote",
+ "myst_line_comment",
+ line_comment,
+ {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+ )
+ md.block.ruler.before(
+ "hr",
+ "myst_block_break",
+ block_break,
+ {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+ )
+ md.block.ruler.before(
+ "hr",
+ "myst_target",
+ target,
+ {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
+ )
+ md.add_render_rule("myst_target", render_myst_target)
+ md.add_render_rule("myst_line_comment", render_myst_line_comment)
+
+
+def line_comment(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ if state.src[pos] != "%":
+ return False
+
+ if silent:
+ return True
+
+ token = state.push("myst_line_comment", "", 0)
+ token.attrSet("class", "myst-line-comment")
+ token.content = state.src[pos + 1 : maximum].rstrip()
+ token.markup = "%"
+
+ # search end of block while appending lines to `token.content`
+ for nextLine in itertools.count(startLine + 1):
+ if nextLine >= endLine:
+ break
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if state.src[pos] != "%":
+ break
+ token.content += "\n" + state.src[pos + 1 : maximum].rstrip()
+
+ state.line = nextLine
+ token.map = [startLine, nextLine]
+
+ return True
+
+
+def block_break(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ marker = state.srcCharCode[pos]
+ pos += 1
+
+ # Check block marker /* + */
+ if marker != 0x2B:
+ return False
+
+ # markers can be mixed with spaces, but there should be at least 3 of them
+
+ cnt = 1
+ while pos < maximum:
+ ch = state.srcCharCode[pos]
+ if ch != marker and not isSpace(ch):
+ break
+ if ch == marker:
+ cnt += 1
+ pos += 1
+
+ if cnt < 3:
+ return False
+
+ if silent:
+ return True
+
+ state.line = startLine + 1
+
+ token = state.push("myst_block_break", "hr", 0)
+ token.attrSet("class", "myst-block")
+ token.content = state.src[pos:maximum].strip()
+ token.map = [startLine, state.line]
+ token.markup = chr(marker) * cnt
+
+ return True
+
+
+def target(state: StateBlock, startLine: int, endLine: int, silent: bool):
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ text = state.src[pos:maximum].strip()
+ if not text.startswith("("):
+ return False
+ if not text.endswith(")="):
+ return False
+ if not text[1:-2]:
+ return False
+
+ if silent:
+ return True
+
+ state.line = startLine + 1
+
+ token = state.push("myst_target", "", 0)
+ token.attrSet("class", "myst-target")
+ token.content = text[1:-2]
+ token.map = [startLine, state.line]
+
+ return True
+
+
+def render_myst_target(self, tokens, idx, options, env):
+ label = tokens[idx].content
+ class_name = "myst-target"
+ target = f'<a href="#{label}">({label})=</a>'
+ return f'<div class="{class_name}">{target}</div>'
+
+
+def render_myst_line_comment(self, tokens, idx, options, env):
+ # Strip leading whitespace from all lines
+ content = "\n".join(line.lstrip() for line in tokens[idx].content.split("\n"))
+ return f"<!-- {escapeHtml(content)} -->"
diff --git a/mdit_py_plugins/myst_role/__init__.py b/mdit_py_plugins/myst_role/__init__.py
new file mode 100644
index 0000000..67b87f8
--- /dev/null
+++ b/mdit_py_plugins/myst_role/__init__.py
@@ -0,0 +1 @@
+from .index import myst_role_plugin # noqa: F401
diff --git a/mdit_py_plugins/myst_role/index.py b/mdit_py_plugins/myst_role/index.py
new file mode 100644
index 0000000..53ef60e
--- /dev/null
+++ b/mdit_py_plugins/myst_role/index.py
@@ -0,0 +1,65 @@
+import re
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import escapeHtml
+from markdown_it.rules_inline import StateInline
+
+VALID_NAME_PATTERN = re.compile(r"^\{([a-zA-Z0-9\_\-\+\:]+)\}")
+
+
+def myst_role_plugin(md: MarkdownIt):
+ """Parse ``{role-name}`content```"""
+ md.inline.ruler.before("backticks", "myst_role", myst_role)
+ md.add_render_rule("myst_role", render_myst_role)
+
+
+def myst_role(state: StateInline, silent: bool):
+
+ # check name
+ match = VALID_NAME_PATTERN.match(state.src[state.pos :])
+ if not match:
+ return False
+ name = match.group(1)
+
+ # check for starting backslash escape
+ try:
+ if state.srcCharCode[state.pos - 1] == 0x5C: # /* \ */
+ # escaped (this could be improved in the case of edge case '\\{')
+ return False
+ except IndexError:
+ pass
+
+ # scan opening tick length
+ start = pos = state.pos + match.end()
+ try:
+ while state.src[pos] == "`":
+ pos += 1
+ except IndexError:
+ return False
+
+ tick_length = pos - start
+ if not tick_length:
+ return False
+
+ # search for closing ticks
+ match = re.search("`" * tick_length, state.src[pos + 1 :])
+ if not match:
+ return False
+ content = state.src[pos : pos + match.start() + 1].replace("\n", " ")
+
+ if not silent:
+ token = state.push("myst_role", "", 0)
+ token.meta = {"name": name}
+ token.content = content
+
+ state.pos = pos + match.end() + 1
+
+ return True
+
+
+def render_myst_role(self, tokens, idx, options, env):
+ token = tokens[idx]
+ name = token.meta.get("name", "unknown")
+ return (
+ '<code class="myst role">' f"{{{name}}}[{escapeHtml(token.content)}]" "</code>"
+ )
diff --git a/mdit_py_plugins/py.typed b/mdit_py_plugins/py.typed
new file mode 100644
index 0000000..7632ecf
--- /dev/null
+++ b/mdit_py_plugins/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561
diff --git a/mdit_py_plugins/substitution.py b/mdit_py_plugins/substitution.py
new file mode 100644
index 0000000..bae120b
--- /dev/null
+++ b/mdit_py_plugins/substitution.py
@@ -0,0 +1,113 @@
+from markdown_it import MarkdownIt
+from markdown_it.rules_block import StateBlock
+from markdown_it.rules_inline import StateInline
+
+
+def substitution_plugin(
+ md: MarkdownIt, start_delimiter: str = "{", end_delimiter: str = "}"
+):
+ """A plugin to create substitution tokens.
+
+ These, token should be handled by the renderer.
+
+ Example::
+
+ {{ block }}
+
+ a {{ inline }} b
+
+ """
+
+ start_char = ord(start_delimiter)
+ end_char = ord(end_delimiter)
+
+ def _substitution_inline(state: StateInline, silent: bool):
+ try:
+ if (
+ state.srcCharCode[state.pos] != start_char
+ or state.srcCharCode[state.pos + 1] != start_char
+ ):
+ return False
+ except IndexError:
+ return False
+
+ pos = state.pos + 2
+ found_closing = False
+ while True:
+ try:
+ end = state.srcCharCode.index(end_char, pos)
+ except ValueError:
+ return False
+ try:
+ if state.srcCharCode[end + 1] == end_char:
+ found_closing = True
+ break
+ except IndexError:
+ return False
+ pos = end + 2
+
+ if not found_closing:
+ return False
+
+ text = state.src[state.pos + 2 : end].strip()
+ state.pos = end + 2
+
+ if silent:
+ return True
+
+ token = state.push("substitution_inline", "span", 0)
+ token.block = False
+ token.content = text
+ token.attrSet("class", "substitution")
+ token.attrSet("text", text)
+ token.markup = f"{start_delimiter}{end_delimiter}"
+
+ return True
+
+ def _substitution_block(
+ state: StateBlock, startLine: int, endLine: int, silent: bool
+ ):
+ startPos = state.bMarks[startLine] + state.tShift[startLine]
+ end = state.eMarks[startLine]
+
+ # if it's indented more than 3 spaces, it should be a code block
+ if state.sCount[startLine] - state.blkIndent >= 4:
+ return False
+
+ lineText = state.src[startPos:end].strip()
+
+ try:
+ if (
+ lineText[0] != start_delimiter
+ or lineText[1] != start_delimiter
+ or lineText[-1] != end_delimiter
+ or lineText[-2] != end_delimiter
+ or len(lineText) < 5
+ ):
+ return False
+ except IndexError:
+ return False
+
+ text = lineText[2:-2].strip()
+
+ # special case if multiple on same line, e.g. {{a}}{{b}}
+ if (end_delimiter * 2) in text:
+ return False
+
+ state.line = startLine + 1
+
+ if silent:
+ return True
+
+ token = state.push("substitution_block", "div", 0)
+ token.block = True
+ token.content = text
+ token.attrSet("class", "substitution")
+ token.attrSet("text", text)
+ token.markup = f"{start_delimiter}{end_delimiter}"
+ token.map = [startLine, state.line]
+
+ return True
+
+ md.block.ruler.before("fence", "substitution_block", _substitution_block)
+ md.inline.ruler.before("escape", "substitution_inline", _substitution_inline)
diff --git a/mdit_py_plugins/tasklists/__init__.py b/mdit_py_plugins/tasklists/__init__.py
new file mode 100644
index 0000000..40a6d67
--- /dev/null
+++ b/mdit_py_plugins/tasklists/__init__.py
@@ -0,0 +1,153 @@
+"""Builds task/todo lists out of markdown lists with items starting with [ ] or [x]"""
+
+# Ported by Wolmar Nyberg Åkerström from https://github.com/revin/markdown-it-task-lists
+# ISC License
+# Copyright (c) 2016, Revin Guillen
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import re
+from typing import List
+from uuid import uuid4
+
+from markdown_it import MarkdownIt
+from markdown_it.token import Token
+
+# Regex string to match a whitespace character, as specified in
+# https://github.github.com/gfm/#whitespace-character
+# (spec version 0.29-gfm (2019-04-06))
+_GFM_WHITESPACE_RE = r"[ \t\n\v\f\r]"
+
+
+def tasklists_plugin(
+ md: MarkdownIt,
+ enabled: bool = False,
+ label: bool = False,
+ label_after: bool = False,
+):
+ """Plugin for building task/todo lists out of markdown lists with items starting with [ ] or [x]
+ .. Nothing else
+
+ For example::
+ - [ ] An item that needs doing
+ - [x] An item that is complete
+
+ The rendered HTML checkboxes are disabled; to change this, pass a truthy value into the enabled
+ property of the plugin options.
+
+ :param enabled: True enables the rendered checkboxes
+ :param label: True wraps the rendered list items in a <label> element for UX purposes,
+ :param label_after: True – adds the <label> element after the checkbox.
+ """
+ disable_checkboxes = not enabled
+ use_label_wrapper = label
+ use_label_after = label_after
+
+ def fcn(state):
+ tokens: List[Token] = state.tokens
+ for i in range(2, len(tokens) - 1):
+
+ if is_todo_item(tokens, i):
+ todoify(tokens[i], tokens[i].__class__)
+ tokens[i - 2].attrSet(
+ "class",
+ "task-list-item" + (" enabled" if not disable_checkboxes else ""),
+ )
+ tokens[parent_token(tokens, i - 2)].attrSet(
+ "class", "contains-task-list"
+ )
+
+ md.core.ruler.after("inline", "github-tasklists", fcn)
+
+ def parent_token(tokens, index):
+ target_level = tokens[index].level - 1
+ for i in range(1, index + 1):
+ if tokens[index - i].level == target_level:
+ return index - i
+ return -1
+
+ def is_todo_item(tokens, index):
+ return (
+ is_inline(tokens[index])
+ and is_paragraph(tokens[index - 1])
+ and is_list_item(tokens[index - 2])
+ and starts_with_todo_markdown(tokens[index])
+ )
+
+ def todoify(token: Token, token_constructor):
+ assert token.children is not None
+ token.children.insert(0, make_checkbox(token, token_constructor))
+ token.children[1].content = token.children[1].content[3:]
+ token.content = token.content[3:]
+
+ if use_label_wrapper:
+ if use_label_after:
+ token.children.pop()
+
+ # Replaced number generator from original plugin with uuid.
+ checklist_id = f"task-item-{uuid4()}"
+ token.children[0].content = (
+ token.children[0].content[0:-1] + f' id="{checklist_id}">'
+ )
+ token.children.append(
+ after_label(token.content, checklist_id, token_constructor)
+ )
+ else:
+ token.children.insert(0, begin_label(token_constructor))
+ token.children.append(end_label(token_constructor))
+
+ def make_checkbox(token, token_constructor):
+ checkbox = token_constructor("html_inline", "", 0)
+ disabled_attr = 'disabled="disabled"' if disable_checkboxes else ""
+ if token.content.startswith("[ ] "):
+ checkbox.content = (
+ '<input class="task-list-item-checkbox" '
+ f'{disabled_attr} type="checkbox">'
+ )
+ elif token.content.startswith("[x] ") or token.content.startswith("[X] "):
+ checkbox.content = (
+ '<input class="task-list-item-checkbox" checked="checked" '
+ f'{disabled_attr} type="checkbox">'
+ )
+ return checkbox
+
+ def begin_label(token_constructor):
+ token = token_constructor("html_inline", "", 0)
+ token.content = "<label>"
+ return token
+
+ def end_label(token_constructor):
+ token = token_constructor("html_inline", "", 0)
+ token.content = "</label>"
+ return token
+
+ def after_label(content, checkbox_id, token_constructor):
+ token = token_constructor("html_inline", "", 0)
+ token.content = (
+ f'<label class="task-list-item-label" for="{checkbox_id}">{content}</label>'
+ )
+ token.attrs = [{"for": checkbox_id}]
+ return token
+
+ def is_inline(token):
+ return token.type == "inline"
+
+ def is_paragraph(token):
+ return token.type == "paragraph_open"
+
+ def is_list_item(token):
+ return token.type == "list_item_open"
+
+ def starts_with_todo_markdown(token):
+ # leading whitespace in a list item is already trimmed off by markdown-it
+ return re.match(rf"\[[ xX]]{_GFM_WHITESPACE_RE}+", token.content)
diff --git a/mdit_py_plugins/tasklists/port.yaml b/mdit_py_plugins/tasklists/port.yaml
new file mode 100644
index 0000000..4ad6da5
--- /dev/null
+++ b/mdit_py_plugins/tasklists/port.yaml
@@ -0,0 +1,6 @@
+- package: markdown-it-task-lists
+ commit: 8233e000559fae5a6306009e55332a54a9d3f606
+ date: 6 Mar 2018
+ version: 2.1.1
+ changes:
+ - Replaced number generator from original plugin with uuid
diff --git a/mdit_py_plugins/texmath/LICENSE b/mdit_py_plugins/texmath/LICENSE
new file mode 100644
index 0000000..b88387c
--- /dev/null
+++ b/mdit_py_plugins/texmath/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2013-17 Stefan Goessner
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/mdit_py_plugins/texmath/README.md b/mdit_py_plugins/texmath/README.md
new file mode 100644
index 0000000..f79f335
--- /dev/null
+++ b/mdit_py_plugins/texmath/README.md
@@ -0,0 +1,137 @@
+[![License](https://img.shields.io/github/license/goessner/markdown-it-texmath.svg)](https://github.com/goessner/markdown-it-texmath/blob/master/licence.txt)
+[![npm](https://img.shields.io/npm/v/markdown-it-texmath.svg)](https://www.npmjs.com/package/markdown-it-texmath)
+[![npm](https://img.shields.io/npm/dt/markdown-it-texmath.svg)](https://www.npmjs.com/package/markdown-it-texmath)
+
+# markdown-it-texmath
+
+Add TeX math equations to your Markdown documents rendered by [markdown-it](https://github.com/markdown-it/markdown-it) parser. [KaTeX](https://github.com/Khan/KaTeX) is used as a fast math renderer.
+
+## Features
+Simplify the process of authoring markdown documents containing math formulas.
+This extension is a comfortable tool for scientists, engineers and students with markdown as their first choice document format.
+
+* Macro support
+* Simple formula numbering
+* Inline math with tables, lists and blockquote.
+* User setting delimiters:
+ * `'dollars'` (default)
+ * inline: `$...$`
+ * display: `$$...$$`
+ * display + equation number: `$$...$$ (1)`
+ * `'brackets'`
+ * inline: `\(...\)`
+ * display: `\[...\]`
+ * display + equation number: `\[...\] (1)`
+ * `'gitlab'`
+ * inline: ``$`...`$``
+ * display: `` ```math ... ``` ``
+ * display + equation number: `` ```math ... ``` (1)``
+ * `'julia'`
+ * inline: `$...$` or ``` ``...`` ```
+ * display: `` ```math ... ``` ``
+ * display + equation number: `` ```math ... ``` (1)``
+ * `'kramdown'`
+ * inline: ``$$...$$``
+ * display: `$$...$$`
+ * display + equation number: `$$...$$ (1)`
+
+## Show me
+
+View a [test table](https://goessner.github.io/markdown-it-texmath/index.html).
+
+[try it out ...](https://goessner.github.io/markdown-it-texmath/markdown-it-texmath-demo.html)
+
+## Use with `node.js`
+
+Install the extension. Verify having `markdown-it` and `katex` already installed .
+```
+npm install markdown-it-texmath
+```
+Use it with JavaScript.
+```js
+let kt = require('katex'),
+ tm = require('markdown-it-texmath').use(kt),
+ md = require('markdown-it')().use(tm,{delimiters:'dollars',macros:{"\\RR": "\\mathbb{R}"}});
+
+md.render('Euler\'s identity \(e^{i\pi}+1=0\) is a beautiful formula in $\\RR 2$.')
+```
+
+## Use in Browser
+```html
+<html>
+<head>
+ <meta charset='utf-8'>
+ <link rel="stylesheet" href="katex.min.css">
+ <link rel="stylesheet" href="texmath.css">
+ <script src="markdown-it.min.js"></script>
+ <script src="katex.min.js"></script>
+ <script src="texmath.js"></script>
+</head>
+<body>
+ <div id="out"></div>
+ <script>
+ let md;
+ document.addEventListener("DOMContentLoaded", () => {
+ const tm = texmath.use(katex);
+ md = markdownit().use(tm,{delimiters:'dollars',macros:{"\\RR": "\\mathbb{R}"}});
+ out.innerHTML = md.render('Euler\'s identity $e^{i\pi}+1=0$ is a beautiful formula in //RR 2.');
+ })
+ </script>
+</body>
+</html>
+```
+## CDN
+
+Use following links for `texmath.js` and `texmath.css`
+* `https://gitcdn.xyz/cdn/goessner/markdown-it-texmath/master/texmath.js`
+* `https://gitcdn.xyz/cdn/goessner/markdown-it-texmath/master/texmath.css`
+
+## Dependencies
+
+* [`markdown-it`](https://github.com/markdown-it/markdown-it): Markdown parser done right. Fast and easy to extend.
+* [`katex`](https://github.com/Khan/KaTeX): This is where credits for fast rendering TeX math in HTML go to.
+
+## ToDo
+
+ nothing yet
+
+## FAQ
+
+* __`markdown-it-texmath` with React Native does not work, why ?__
+ * `markdown-it-texmath` is using regular expressions with `y` [(sticky) property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky) and cannot avoid this. The use of the `y` flag in regular expressions means the plugin is not compatible with React Native (which as of now doesn't support it and throws an error `Invalid flags supplied to RegExp constructor`).
+
+## CHANGELOG
+
+### [0.6.0] on October 04, 2019
+* Add support for [Julia Markdown](https://docs.julialang.org/en/v1/stdlib/Markdown/) on [request](https://github.com/goessner/markdown-it-texmath/issues/15).
+
+### [0.5.5] on February 07, 2019
+* Remove [rendering bug with brackets delimiters](https://github.com/goessner/markdown-it-texmath/issues/9).
+
+### [0.5.4] on January 20, 2019
+* Remove pathological [bug within blockquotes](https://github.com/goessner/mdmath/issues/50).
+
+### [0.5.3] on November 11, 2018
+* Add support for Tex macros (https://katex.org/docs/supported.html#macros) .
+* Bug with [brackets delimiters](https://github.com/goessner/markdown-it-texmath/issues/9) .
+
+### [0.5.2] on September 07, 2018
+* Add support for [Kramdown](https://kramdown.gettalong.org/) .
+
+### [0.5.0] on August 15, 2018
+* Fatal blockquote bug investigated. Implemented workaround to vscode bug, which has finally gone with vscode 1.26.0 .
+
+### [0.4.6] on January 05, 2018
+* Escaped underscore bug removed.
+
+### [0.4.5] on November 06, 2017
+* Backslash bug removed.
+
+### [0.4.4] on September 27, 2017
+* Modifying the `block` mode regular expression with `gitlab` delimiters, so removing the `newline` bug.
+
+## License
+
+`markdown-it-texmath` is licensed under the [MIT License](./license.txt)
+
+ © [Stefan Gössner](https://github.com/goessner)
diff --git a/mdit_py_plugins/texmath/__init__.py b/mdit_py_plugins/texmath/__init__.py
new file mode 100644
index 0000000..f0c2588
--- /dev/null
+++ b/mdit_py_plugins/texmath/__init__.py
@@ -0,0 +1 @@
+from .index import texmath_plugin # noqa F401
diff --git a/mdit_py_plugins/texmath/index.py b/mdit_py_plugins/texmath/index.py
new file mode 100644
index 0000000..ecf178c
--- /dev/null
+++ b/mdit_py_plugins/texmath/index.py
@@ -0,0 +1,307 @@
+import re
+from typing import Optional
+
+from markdown_it import MarkdownIt
+from markdown_it.common.utils import charCodeAt
+
+
+def texmath_plugin(md: MarkdownIt, delimiters="dollars", macros: Optional[dict] = None):
+ """Plugin ported from
+ `markdown-it-texmath <https://github.com/goessner/markdown-it-texmath>`__.
+
+ It parses TeX math equations set inside opening and closing delimiters:
+
+ .. code-block:: md
+
+ $\\alpha = \\frac{1}{2}$
+
+ :param delimiters: one of: brackets, dollars, gitlab, julia, kramdown
+
+ """
+ macros = macros or {}
+
+ if delimiters in rules:
+ for rule_inline in rules[delimiters]["inline"]:
+ md.inline.ruler.before(
+ "escape", rule_inline["name"], make_inline_func(rule_inline)
+ )
+
+ def render_math_inline(self, tokens, idx, options, env):
+ return rule_inline["tmpl"].format(
+ render(tokens[idx].content, False, macros)
+ )
+
+ md.add_render_rule(rule_inline["name"], render_math_inline)
+
+ for rule_block in rules[delimiters]["block"]:
+ md.block.ruler.before(
+ "fence", rule_block["name"], make_block_func(rule_block)
+ )
+
+ def render_math_block(self, tokens, idx, options, env):
+ return rule_block["tmpl"].format(
+ render(tokens[idx].content, True, macros), tokens[idx].info
+ )
+
+ md.add_render_rule(rule_block["name"], render_math_block)
+
+
+def applyRule(rule, string: str, begin, inBlockquote):
+
+ if not (
+ string.startswith(rule["tag"], begin)
+ and (rule["pre"](string, begin) if "pre" in rule else True)
+ ):
+ return False
+
+ match = rule["rex"].match(string[begin:]) # type: re.Match
+
+ if not match or match.start() != 0:
+ return False
+
+ lastIndex = match.end() + begin - 1
+ if "post" in rule:
+ if not (
+ rule["post"](string, lastIndex) # valid post-condition
+ # remove evil blockquote bug (https:#github.com/goessner/mdmath/issues/50)
+ and (not inBlockquote or "\n" not in match.group(1))
+ ):
+ return False
+ return match
+
+
+def make_inline_func(rule):
+ def _func(state, silent):
+ res = applyRule(rule, state.src, state.pos, False)
+ if res:
+ if not silent:
+ token = state.push(rule["name"], "math", 0)
+ token.content = res[1] # group 1 from regex ..
+ token.markup = rule["tag"]
+
+ state.pos += res.end()
+
+ return bool(res)
+
+ return _func
+
+
+def make_block_func(rule):
+ def _func(state, begLine, endLine, silent):
+ begin = state.bMarks[begLine] + state.tShift[begLine]
+ res = applyRule(rule, state.src, begin, state.parentType == "blockquote")
+ if res:
+ if not silent:
+ token = state.push(rule["name"], "math", 0)
+ token.block = True
+ token.content = res[1]
+ token.info = res[len(res.groups())]
+ token.markup = rule["tag"]
+
+ line = begLine
+ endpos = begin + res.end() - 1
+
+ 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
+
+ state.pos = begin + res.end()
+
+ return bool(res)
+
+ return _func
+
+
+def dollar_pre(str, beg):
+ prv = charCodeAt(str[beg - 1], 0) if beg > 0 else False
+ return (
+ (not prv) or prv != 0x5C and (prv < 0x30 or prv > 0x39) # no backslash,
+ ) # no decimal digit .. before opening '$'
+
+
+def dollar_post(string, end):
+ try:
+ nxt = string[end + 1] and charCodeAt(string[end + 1], 0)
+ except IndexError:
+ return True
+ return (
+ (not nxt) or (nxt < 0x30) or (nxt > 0x39)
+ ) # no decimal digit .. after closing '$'
+
+
+def render(tex, displayMode, macros):
+ return tex
+ # TODO better HTML renderer port for math
+ # try:
+ # res = katex.renderToString(tex,{throwOnError:False,displayMode,macros})
+ # except:
+ # res = tex+": "+err.message.replace("<","&lt;")
+ # return res
+
+
+# def use(katex): # math renderer used ...
+# texmath.katex = katex; # ... katex solely at current ...
+# return texmath;
+# }
+
+
+# All regexes areg global (g) and sticky (y), see:
+# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
+
+rules: dict = {
+ "brackets": {
+ "inline": [
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^\\\((.+?)\\\)", re.DOTALL),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "\\(",
+ }
+ ],
+ "block": [
+ {
+ "name": "math_block_eqno",
+ "rex": re.compile(
+ r"^\\\[(((?!\\\]|\\\[)[\s\S])+?)\\\]\s*?\(([^)$\r\n]+?)\)", re.M
+ ),
+ "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
+ "tag": "\\[",
+ },
+ {
+ "name": "math_block",
+ "rex": re.compile(r"^\\\[([\s\S]+?)\\\]", re.M),
+ "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
+ "tag": "\\[",
+ },
+ ],
+ },
+ "gitlab": {
+ "inline": [
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^\$`(.+?)`\$"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$`",
+ }
+ ],
+ "block": [
+ {
+ "name": "math_block_eqno",
+ "rex": re.compile(
+ r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
+ ),
+ "tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n', # noqa: E501
+ "tag": "```math",
+ },
+ {
+ "name": "math_block",
+ "rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
+ "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
+ "tag": "```math",
+ },
+ ],
+ },
+ "julia": {
+ "inline": [
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^`{2}([^`]+?)`{2}"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "``",
+ },
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^\$(\S[^$\r\n]*?[^\s\\]{1}?)\$"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$",
+ "pre": dollar_pre,
+ "post": dollar_post,
+ },
+ {
+ "name": "math_single",
+ "rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$",
+ "pre": dollar_pre,
+ "post": dollar_post,
+ },
+ ],
+ "block": [
+ {
+ "name": "math_block_eqno",
+ "rex": re.compile(
+ r"^`{3}math\s+?([^`]+?)\s+?`{3}\s*?\(([^)$\r\n]+?)\)", re.M
+ ),
+ "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
+ "tag": "```math",
+ },
+ {
+ "name": "math_block",
+ "rex": re.compile(r"^`{3}math\s+?([^`]+?)\s+?`{3}", re.M),
+ "tmpl": "<section><eqn>{0}</eqn></section>",
+ "tag": "```math",
+ },
+ ],
+ },
+ "kramdown": {
+ "inline": [
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^\${2}([^$\r\n]*?)\${2}"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$$",
+ }
+ ],
+ "block": [
+ {
+ "name": "math_block_eqno",
+ "rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
+ "tmpl": '<section class="eqno"><eqn>{0}</eqn><span>({1})</span></section>',
+ "tag": "$$",
+ },
+ {
+ "name": "math_block",
+ "rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
+ "tmpl": "<section><eqn>{0}</eqn></section>",
+ "tag": "$$",
+ },
+ ],
+ },
+ "dollars": {
+ "inline": [
+ {
+ "name": "math_inline",
+ "rex": re.compile(r"^\$(\S[^$]*?[^\s\\]{1}?)\$"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$",
+ "pre": dollar_pre,
+ "post": dollar_post,
+ },
+ {
+ "name": "math_single",
+ "rex": re.compile(r"^\$([^$\s\\]{1}?)\$"),
+ "tmpl": "<eq>{0}</eq>",
+ "tag": "$",
+ "pre": dollar_pre,
+ "post": dollar_post,
+ },
+ ],
+ "block": [
+ {
+ "name": "math_block_eqno",
+ "rex": re.compile(r"^\${2}([^$]*?)\${2}\s*?\(([^)$\r\n]+?)\)", re.M),
+ "tmpl": '<section class="eqno">\n<eqn>{0}</eqn><span>({1})</span>\n</section>\n', # noqa: E501
+ "tag": "$$",
+ },
+ {
+ "name": "math_block",
+ "rex": re.compile(r"^\${2}([^$]*?)\${2}", re.M),
+ "tmpl": "<section>\n<eqn>{0}</eqn>\n</section>\n",
+ "tag": "$$",
+ },
+ ],
+ },
+}
diff --git a/mdit_py_plugins/texmath/port.yaml b/mdit_py_plugins/texmath/port.yaml
new file mode 100644
index 0000000..ba47ac8
--- /dev/null
+++ b/mdit_py_plugins/texmath/port.yaml
@@ -0,0 +1,7 @@
+- package: markdown-it-texmath
+ commit: 78c548829ce2ef85c73dc71e680d01e5ae41ffbf
+ date: Oct 4, 2019
+ version: 0.6
+ changes: |
+ both dollars/math_inline and brackets/math_inline regexes have been changed,
+ to allow (single) line breaks
diff --git a/mdit_py_plugins/wordcount/__init__.py b/mdit_py_plugins/wordcount/__init__.py
new file mode 100644
index 0000000..577eeda
--- /dev/null
+++ b/mdit_py_plugins/wordcount/__init__.py
@@ -0,0 +1,58 @@
+import string
+from typing import Callable, List
+
+from markdown_it import MarkdownIt
+from markdown_it.rules_core import StateCore
+
+
+def basic_count(text: str) -> int:
+ """Split the string and ignore punctuation only elements."""
+ return sum([el.strip(string.punctuation).isalpha() for el in text.split()])
+
+
+def wordcount_plugin(
+ md: MarkdownIt,
+ *,
+ per_minute: int = 200,
+ count_func: Callable[[str], int] = basic_count,
+ store_text: bool = False
+):
+ """Plugin for computing and storing the word count.
+
+ Stores in the ``env`` e.g.::
+
+ env["wordcount"] = {
+ "words": 200
+ "minutes": 1,
+ }
+
+ If "wordcount" is already in the env, it will update it.
+
+ :param per_minute: Words per minute reading speed
+ :param store_text: store all text under a "text" key, as a list of strings
+ """
+
+ def _word_count_rule(state: StateCore) -> None:
+ text: List[str] = []
+ words = 0
+ for token in state.tokens:
+ if token.type == "text":
+ words += count_func(token.content)
+ if store_text:
+ text.append(token.content)
+ elif token.type == "inline":
+ for child in token.children or ():
+ if child.type == "text":
+ words += count_func(child.content)
+ if store_text:
+ text.append(child.content)
+
+ data = state.env.setdefault("wordcount", {})
+ if store_text:
+ data.setdefault("text", [])
+ data["text"] += text
+ data.setdefault("words", 0)
+ data["words"] += words
+ data["minutes"] = int(round(data["words"] / per_minute))
+
+ md.core.ruler.push("wordcount", _word_count_rule)