From 4c793d598bcc52ce141a57909c1f40cfb97b3bc4 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sat, 26 Nov 2022 10:10:29 -0500 Subject: [PATCH 01/11] feat: port the admon plugin --- mdit_py_plugins/admon/LICENSE | 24 +++ mdit_py_plugins/admon/__init__.py | 1 + mdit_py_plugins/admon/index.py | 190 +++++++++++++++++++++ mdit_py_plugins/admon/port.yaml | 4 + tests/fixtures/admon.md | 269 ++++++++++++++++++++++++++++++ tests/test_admon.py | 22 +++ 6 files changed, 510 insertions(+) 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 tests/fixtures/admon.md create mode 100644 tests/test_admon.py 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..89df3a7 --- /dev/null +++ b/mdit_py_plugins/admon/index.py @@ -0,0 +1,190 @@ +# Process admonitions and pass to cb. + +from typing import Dict, Optional, Tuple +import math +from markdown_it import MarkdownIt +from markdown_it.rules_block import StateBlock + +ADMONITION_TAGS = [ + "note", + "summary", + "abstract", + "tldr", + "info", + "todo", + "tip", + "hint", + "success", + "check", + "done", + "question", + "help", + "faq", + "warning", + "attention", + "caution", + "failure", + "fail", + "missing", + "danger", + "error", + "bug", + "example", + "snippet", + "quote", + "cite", +] + + +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 "" + if tag.lower() in ADMONITION_TAGS: + return True + # FIXME: Should this return False? Or better way to log warnings? + print(f"Warning: admonition tag {tag} is not one of: {ADMONITION_TAGS}") + return bool(tag) + + +def render_default(tokens, idx, _options, env, slf): + return slf.renderToken(tokens, idx, _options, env, slf) + + +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): + 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 + 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) -> None: + """Plugin ported from: + + `markdown-it-admon `. + + """ + 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..ab2a5bc --- /dev/null +++ b/mdit_py_plugins/admon/port.yaml @@ -0,0 +1,4 @@ +- package: markdown-it-admon + commit: 9820ba89415c464a3cc18a780f222a0ceb3e18bd + date: Nov 26, 2022 + 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..4f1ea80 --- /dev/null +++ b/tests/test_admon.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from markdown_it import MarkdownIt +from markdown_it.token import Token +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() From 20c8beb8e561e712537838875a95dfeee227b6f0 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sat, 26 Nov 2022 10:22:06 -0500 Subject: [PATCH 02/11] ci: resolve flake8 and mypy errors --- mdit_py_plugins/admon/index.py | 7 ++++--- tests/test_admon.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index 89df3a7..ec5cbe9 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -1,7 +1,8 @@ # Process admonitions and pass to cb. -from typing import Dict, Optional, Tuple import math +from typing import Tuple + from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock @@ -142,7 +143,7 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool): token.markup = markup token.block = True token.attrs = {"class": f"admonition {tag}"} - token.meta = tag + token.meta = {"tag": tag} token.content = title token.info = params token.map = [startLine, next_line] @@ -179,7 +180,7 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool): def admon_plugin(md: MarkdownIt) -> None: """Plugin ported from: - `markdown-it-admon `. + `markdown-it-admon `. """ md.block.ruler.before( diff --git a/tests/test_admon.py b/tests/test_admon.py index 4f1ea80..efd0ee8 100644 --- a/tests/test_admon.py +++ b/tests/test_admon.py @@ -1,7 +1,6 @@ from pathlib import Path from markdown_it import MarkdownIt -from markdown_it.token import Token from markdown_it.utils import read_fixture_file import pytest From 4cf72e96ece5d2ddcb4269deca5938a7111a93ae Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Nov 2022 08:42:19 -0500 Subject: [PATCH 03/11] refactor: strictly enforce tag names --- mdit_py_plugins/admon/index.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index ec5cbe9..9a932e8 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -54,11 +54,7 @@ def get_tag(params: str) -> Tuple[str, str]: def validate(params: str) -> bool: tag = params.strip().split(" ", 1)[-1] or "" - if tag.lower() in ADMONITION_TAGS: - return True - # FIXME: Should this return False? Or better way to log warnings? - print(f"Warning: admonition tag {tag} is not one of: {ADMONITION_TAGS}") - return bool(tag) + return tag.lower() in ADMONITION_TAGS def render_default(tokens, idx, _options, env, slf): From fb46831aa97ddce09e5d4269a2d25635a707f26b Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Nov 2022 08:46:54 -0500 Subject: [PATCH 04/11] fix: remove strict matching of admonition titles --- mdit_py_plugins/admon/index.py | 61 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index 9a932e8..b9f0f6f 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -6,35 +6,36 @@ from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock -ADMONITION_TAGS = [ - "note", - "summary", - "abstract", - "tldr", - "info", - "todo", - "tip", - "hint", - "success", - "check", - "done", - "question", - "help", - "faq", - "warning", - "attention", - "caution", - "failure", - "fail", - "missing", - "danger", - "error", - "bug", - "example", - "snippet", - "quote", - "cite", -] +# FYI: The admonition tags are not used for validation +# ADMONITION_TAGS = [ +# "note", +# "summary", +# "abstract", +# "tldr", +# "info", +# "todo", +# "tip", +# "hint", +# "success", +# "check", +# "done", +# "question", +# "help", +# "faq", +# "warning", +# "attention", +# "caution", +# "failure", +# "fail", +# "missing", +# "danger", +# "error", +# "bug", +# "example", +# "snippet", +# "quote", +# "cite", +# ] def get_tag(params: str) -> Tuple[str, str]: @@ -54,7 +55,7 @@ def get_tag(params: str) -> Tuple[str, str]: def validate(params: str) -> bool: tag = params.strip().split(" ", 1)[-1] or "" - return tag.lower() in ADMONITION_TAGS + return bool(tag) def render_default(tokens, idx, _options, env, slf): From f2251fcd0d16836414dddd769e31bd7bdede5181 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Nov 2022 08:47:17 -0500 Subject: [PATCH 05/11] fix: add optional renderer like source implementation --- mdit_py_plugins/admon/index.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index b9f0f6f..f1c4444 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -1,7 +1,7 @@ # Process admonitions and pass to cb. import math -from typing import Tuple +from typing import Callable, Optional, Tuple from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock @@ -174,12 +174,30 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool): return True -def admon_plugin(md: MarkdownIt) -> None: +def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None: """Plugin ported from: `markdown-it-admon `. + Plugin for admonitions: + + .. code-block:: md + + !!! note + *content* + """ + + 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", From ab0c898cccef63a1ddf362887d943ac2f79ebdf6 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Nov 2022 08:54:13 -0500 Subject: [PATCH 06/11] docs: add autodoc for Admonitions --- docs/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.md b/docs/index.md index 8b38d31..09b8812 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,6 +37,12 @@ md = MarkdownIt().use(plugin1, keyword=value).use(plugin2, keyword=value) html_string = md.render("some *Markdown*") ``` +## Admonitions + +```{eval-rst} +.. autofunction:: mdit_py_plugins.admon.admon_plugin +``` + ## Front-Matter ```{eval-rst} From 47875583a5ae025d32789dd164490f91539a7d01 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Nov 2022 10:21:07 -0500 Subject: [PATCH 07/11] refactor: remove unused render_default --- mdit_py_plugins/admon/index.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index f1c4444..1ab54df 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -58,10 +58,6 @@ def validate(params: str) -> bool: return bool(tag) -def render_default(tokens, idx, _options, env, slf): - return slf.renderToken(tokens, idx, _options, env, slf) - - MIN_MARKERS = 3 MARKER_STR = "!" MARKER_CHAR = ord(MARKER_STR) From 4fed83fa9373d7f965b238b1eb3abbf26db65713 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Mon, 28 Nov 2022 08:09:47 -0500 Subject: [PATCH 08/11] docs: fix commit date for port --- mdit_py_plugins/admon/port.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdit_py_plugins/admon/port.yaml b/mdit_py_plugins/admon/port.yaml index ab2a5bc..d2835bc 100644 --- a/mdit_py_plugins/admon/port.yaml +++ b/mdit_py_plugins/admon/port.yaml @@ -1,4 +1,4 @@ - package: markdown-it-admon commit: 9820ba89415c464a3cc18a780f222a0ceb3e18bd - date: Nov 26, 2022 + date: Jul 3, 2021 version: 1.0.0 From 1bee3fb4df5d3b1933757bc38b891adad6dd8e4c Mon Sep 17 00:00:00 2001 From: Kyle King Date: Mon, 5 Dec 2022 07:18:17 -0500 Subject: [PATCH 09/11] refactor: apply suggestions from code review Co-authored-by: Chris Sewell --- mdit_py_plugins/admon/index.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index 1ab54df..7309509 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -64,7 +64,7 @@ def validate(params: str) -> bool: MARKER_LEN = len(MARKER_STR) -def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool): +def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: start = state.bMarks[startLine] + state.tShift[startLine] maximum = state.eMarks[startLine] @@ -171,17 +171,18 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool): def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None: - """Plugin ported from: - - `markdown-it-admon `. - - Plugin for admonitions: + """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): From 84eb2c2fc1801fff41fb020a244e9b1ac82c4c26 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Mon, 5 Dec 2022 07:19:50 -0500 Subject: [PATCH 10/11] docs: place under "Containers" Section --- docs/index.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 09b8812..45ffb4c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,12 +37,6 @@ md = MarkdownIt().use(plugin1, keyword=value).use(plugin2, keyword=value) html_string = md.render("some *Markdown*") ``` -## Admonitions - -```{eval-rst} -.. autofunction:: mdit_py_plugins.admon.admon_plugin -``` - ## Front-Matter ```{eval-rst} @@ -91,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} From 073cb243f75dbc2e3fea9809de64fcb70d651fb5 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Mon, 5 Dec 2022 07:22:32 -0500 Subject: [PATCH 11/11] refactor: remove unused ADMONITION_TAGS --- mdit_py_plugins/admon/index.py | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/mdit_py_plugins/admon/index.py b/mdit_py_plugins/admon/index.py index 7309509..8ebbe8f 100644 --- a/mdit_py_plugins/admon/index.py +++ b/mdit_py_plugins/admon/index.py @@ -6,37 +6,6 @@ from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock -# FYI: The admonition tags are not used for validation -# ADMONITION_TAGS = [ -# "note", -# "summary", -# "abstract", -# "tldr", -# "info", -# "todo", -# "tip", -# "hint", -# "success", -# "check", -# "done", -# "question", -# "help", -# "faq", -# "warning", -# "attention", -# "caution", -# "failure", -# "fail", -# "missing", -# "danger", -# "error", -# "bug", -# "example", -# "snippet", -# "quote", -# "cite", -# ] - def get_tag(params: str) -> Tuple[str, str]: if not params.strip(): @@ -181,7 +150,7 @@ def admon_plugin(md: MarkdownIt, render: Optional[Callable] = None) -> None: *content* Note, this is ported from - `markdown-it-admon + `markdown-it-admon `_. """