diff --git a/docs/index.md b/docs/index.md index 8b38d31..45ffb4c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,6 +85,10 @@ html_string = md.render("some *Markdown*") .. autofunction:: mdit_py_plugins.container.container_plugin ``` +```{eval-rst} +.. autofunction:: mdit_py_plugins.admon.admon_plugin +``` + ## Inline Attributes ```{eval-rst} 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/tests/fixtures/admon.md b/tests/fixtures/admon.md new file mode 100644 index 0000000..587ae80 --- /dev/null +++ b/tests/fixtures/admon.md @@ -0,0 +1,269 @@ + +Simple admonition +. +!!! note + *content* +. +
+

Note

+

content

+
+. + + +Could contain block elements too +. +!!! note + ### heading + + ----------- + +. +
+

Note

+

heading

+
+
+. + + +Shows custom title +. +!!! note Custom title + + Some text + +. +
+

Custom title

+

Some text

+
+. + + +Shows no title +. +!!! note "" + Some text + +. +
+

Some text

+
+. + + +Closes block after 2 empty lines +. +!!! note + Some text + + + A code block +. +
+

Note

+

Some text

+
+
A code block
+
+. + + +Nested blocks +. +!!! note + !!! note + Some text + + code block +. +
+

Note

+
+

Note

+

Some text

+
code block
+
+
+
+. + + +Consecutive admonitions +. +!!! note + +!!! warning +. +
+

Note

+
+
+

Warning

+
+. + + +Marker may be indented up to 3 chars +. + !!! note + content +. +
+

Note

+

content

+
+. + + +But that's a code block +. + !!! note + content +. +
!!! note
+    content
+
+. + + +Some more indent checks +. + !!! note + not a code block + + code block +. +
+

Note

+
+

not a code block

+
code block
+
+. + + +Type could be adjacent to marker +. +!!!note + xxx + +. +
+

Note

+

xxx

+
+. + + +Type could be adjacent to marker and content may be shifted up to 3 chars +. +!!!note + xxx + +. +
+

Note

+

xxx

+
+. + + +Or several spaces apart +. +!!! note + xxx +. +
+

Note

+

xxx

+
+. + + +Admonitions self-close at the end of the document +. +!!! note + xxx +. +
+

Note

+

xxx

+
+. + + +They could be nested in lists +. +- !!! note + - a + - b +- !!! warning + - c + - d +. + +. + + +Or in blockquotes +. +> !!! note +> xxx +> > yyy +> zzz +> +. +
+
+

Note

+

xxx

+
+

yyy +zzz

+
+
+
+. + + +Renders unknown admonition type +. +!!! unknown title + content +. +
+

title

+

content

+
+. + + +Does not render +. +!!! + content +. +

!!! +content

+. diff --git a/tests/test_admon.py b/tests/test_admon.py new file mode 100644 index 0000000..efd0ee8 --- /dev/null +++ b/tests/test_admon.py @@ -0,0 +1,21 @@ +from pathlib import Path + +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file +import pytest + +from mdit_py_plugins.admon import admon_plugin + +FIXTURE_PATH = Path(__file__).parent + + +@pytest.mark.parametrize( + "line,title,input,expected", + read_fixture_file(FIXTURE_PATH.joinpath("fixtures", "admon.md")), +) +def test_all(line, title, input, expected): + md = MarkdownIt("commonmark").use(admon_plugin) + md.options["xhtmlOut"] = False + text = md.render(input) + print(text) + assert text.rstrip() == expected.rstrip()