From fcb2f10732db61d216e2105c8154486f66b3e3ff Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 29 Apr 2024 06:29:52 +0200 Subject: Adding upstream version 0.3.3. Signed-off-by: Daniel Baumann --- mdit_py_plugins/__init__.py | 1 + mdit_py_plugins/admon/LICENSE | 24 ++ mdit_py_plugins/admon/__init__.py | 1 + mdit_py_plugins/admon/index.py | 172 +++++++++++++ mdit_py_plugins/admon/port.yaml | 4 + mdit_py_plugins/amsmath/__init__.py | 126 +++++++++ mdit_py_plugins/anchors/__init__.py | 1 + mdit_py_plugins/anchors/index.py | 129 ++++++++++ mdit_py_plugins/attrs/__init__.py | 1 + mdit_py_plugins/attrs/index.py | 123 +++++++++ mdit_py_plugins/attrs/parse.py | 265 +++++++++++++++++++ mdit_py_plugins/colon_fence.py | 132 ++++++++++ mdit_py_plugins/container/LICENSE | 22 ++ mdit_py_plugins/container/README.md | 95 +++++++ mdit_py_plugins/container/__init__.py | 1 + mdit_py_plugins/container/index.py | 174 +++++++++++++ mdit_py_plugins/container/port.yaml | 5 + mdit_py_plugins/deflist/LICENSE | 22 ++ mdit_py_plugins/deflist/README.md | 38 +++ mdit_py_plugins/deflist/__init__.py | 1 + mdit_py_plugins/deflist/index.py | 253 ++++++++++++++++++ mdit_py_plugins/deflist/port.yaml | 5 + mdit_py_plugins/dollarmath/__init__.py | 1 + mdit_py_plugins/dollarmath/index.py | 339 ++++++++++++++++++++++++ mdit_py_plugins/field_list/__init__.py | 208 +++++++++++++++ mdit_py_plugins/footnote/LICENSE | 22 ++ mdit_py_plugins/footnote/__init__.py | 1 + mdit_py_plugins/footnote/index.py | 430 +++++++++++++++++++++++++++++++ mdit_py_plugins/footnote/port.yaml | 4 + mdit_py_plugins/front_matter/LICENSE | 22 ++ mdit_py_plugins/front_matter/__init__.py | 1 + mdit_py_plugins/front_matter/index.py | 138 ++++++++++ mdit_py_plugins/front_matter/port.yaml | 4 + mdit_py_plugins/myst_blocks/__init__.py | 1 + mdit_py_plugins/myst_blocks/index.py | 153 +++++++++++ mdit_py_plugins/myst_role/__init__.py | 1 + mdit_py_plugins/myst_role/index.py | 65 +++++ mdit_py_plugins/py.typed | 1 + mdit_py_plugins/substitution.py | 113 ++++++++ mdit_py_plugins/tasklists/__init__.py | 153 +++++++++++ mdit_py_plugins/tasklists/port.yaml | 6 + mdit_py_plugins/texmath/LICENSE | 21 ++ mdit_py_plugins/texmath/README.md | 137 ++++++++++ mdit_py_plugins/texmath/__init__.py | 1 + mdit_py_plugins/texmath/index.py | 307 ++++++++++++++++++++++ mdit_py_plugins/texmath/port.yaml | 7 + mdit_py_plugins/wordcount/__init__.py | 58 +++++ 47 files changed, 3789 insertions(+) create mode 100644 mdit_py_plugins/__init__.py create mode 100644 mdit_py_plugins/admon/LICENSE create mode 100644 mdit_py_plugins/admon/__init__.py create mode 100644 mdit_py_plugins/admon/index.py create mode 100644 mdit_py_plugins/admon/port.yaml create mode 100644 mdit_py_plugins/amsmath/__init__.py create mode 100644 mdit_py_plugins/anchors/__init__.py create mode 100644 mdit_py_plugins/anchors/index.py create mode 100644 mdit_py_plugins/attrs/__init__.py create mode 100644 mdit_py_plugins/attrs/index.py create mode 100644 mdit_py_plugins/attrs/parse.py create mode 100644 mdit_py_plugins/colon_fence.py create mode 100644 mdit_py_plugins/container/LICENSE create mode 100644 mdit_py_plugins/container/README.md create mode 100644 mdit_py_plugins/container/__init__.py create mode 100644 mdit_py_plugins/container/index.py create mode 100644 mdit_py_plugins/container/port.yaml create mode 100644 mdit_py_plugins/deflist/LICENSE create mode 100644 mdit_py_plugins/deflist/README.md create mode 100644 mdit_py_plugins/deflist/__init__.py create mode 100644 mdit_py_plugins/deflist/index.py create mode 100644 mdit_py_plugins/deflist/port.yaml create mode 100644 mdit_py_plugins/dollarmath/__init__.py create mode 100644 mdit_py_plugins/dollarmath/index.py create mode 100644 mdit_py_plugins/field_list/__init__.py create mode 100644 mdit_py_plugins/footnote/LICENSE create mode 100644 mdit_py_plugins/footnote/__init__.py create mode 100644 mdit_py_plugins/footnote/index.py create mode 100644 mdit_py_plugins/footnote/port.yaml create mode 100644 mdit_py_plugins/front_matter/LICENSE create mode 100644 mdit_py_plugins/front_matter/__init__.py create mode 100644 mdit_py_plugins/front_matter/index.py create mode 100644 mdit_py_plugins/front_matter/port.yaml create mode 100644 mdit_py_plugins/myst_blocks/__init__.py create mode 100644 mdit_py_plugins/myst_blocks/index.py create mode 100644 mdit_py_plugins/myst_role/__init__.py create mode 100644 mdit_py_plugins/myst_role/index.py create mode 100644 mdit_py_plugins/py.typed create mode 100644 mdit_py_plugins/substitution.py create mode 100644 mdit_py_plugins/tasklists/__init__.py create mode 100644 mdit_py_plugins/tasklists/port.yaml create mode 100644 mdit_py_plugins/texmath/LICENSE create mode 100644 mdit_py_plugins/texmath/README.md create mode 100644 mdit_py_plugins/texmath/__init__.py create mode 100644 mdit_py_plugins/texmath/index.py create mode 100644 mdit_py_plugins/texmath/port.yaml create mode 100644 mdit_py_plugins/wordcount/__init__.py (limited to 'mdit_py_plugins') 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 + `_. + + .. code-block:: md + + !!! note + *content* + + Note, this is ported from + `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 `__ 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'
\n{content}\n
\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 `__ + + .. code-block:: md + + # Title String + + renders as: + + .. code-block:: html + +

Title String

+ + :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 + `_. + + 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 ( + "
"
+        + content
+        + "
\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, `
` with +container name class will be created: + +```html +
+here be dragons +
+``` + +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 '
' + md.utils.escapeHtml(m[1]) + '\n'; + + } else { + // closing tag + return '
\n'; + } + } +}); + +console.log(md.render('::: spoiler click me\n*content*\n:::\n')); + +// Output: +// +//
click me +//

content

+//
+``` + +## 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 `__. + + 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 (`
`) 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 `__. + + The syntax is based on + `pandoc 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'' # 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'{content}' + + def render_math_inline_double(self, tokens, idx, options, env) -> str: + content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True}) + return f'
{content}
' + + def render_math_block(self, tokens, idx, options, env) -> str: + content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True}) + return f'
\n{content}\n
\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'
\n{label}\n{content}\n
\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 + `_. + + .. 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 `__. + + It is based on the + `pandoc definition `__: + + .. 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 ( + '' + + caption + + "" + ) + + +def render_footnote_block_open(self, tokens, idx, options, env): + return ( + ( + '
\n' + if options.xhtmlOut + else '
\n' + ) + + '
\n' + + '
    \n' + ) + + +def render_footnote_block_close(self, tokens, idx, options, env): + return "
\n
\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 '
  • ' + + +def render_footnote_close(self, tokens, idx, options, env): + return "
  • \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 ' \u21a9\uFE0E' 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 `__. + + 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'({label})=' + return f'
    {target}
    ' + + +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"" 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 ( + '' f"{{{name}}}[{escapeHtml(token.content)}]" "" + ) 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