From f6a71eb896c2ed173cbb9aead9f5f92cc5630027 Mon Sep 17 00:00:00 2001 From: Mitesh Ashar Date: Sun, 29 Sep 2024 14:34:13 +0530 Subject: [PATCH] :sparkles: NEW: Add plugin & tests to render subscripts --- docs/index.md | 6 ++ mdit_py_plugins/subscript/__init__.py | 117 +++++++++++++++++++++++ mdit_py_plugins/subscript/port.yaml | 8 ++ tests/fixtures/subscript.md | 130 ++++++++++++++++++++++++++ tests/test_subscript.py | 23 +++++ 5 files changed, 284 insertions(+) create mode 100644 mdit_py_plugins/subscript/__init__.py create mode 100644 mdit_py_plugins/subscript/port.yaml create mode 100644 tests/fixtures/subscript.md create mode 100644 tests/test_subscript.py diff --git a/docs/index.md b/docs/index.md index 71846de..ea43f45 100644 --- a/docs/index.md +++ b/docs/index.md @@ -113,6 +113,12 @@ html_string = md.render("some *Markdown*") .. autofunction:: mdit_py_plugins.amsmath.amsmath_plugin ``` +## Subscripts + +```{eval-rst} +.. autofunction:: mdit_py_plugins.subscript.sub_plugin +``` + ## MyST plugins `myst_blocks` and `myst_role` plugins are also available, for utilisation by the [MyST renderer](https://myst-parser.readthedocs.io/en/latest/using/syntax.html) diff --git a/mdit_py_plugins/subscript/__init__.py b/mdit_py_plugins/subscript/__init__.py new file mode 100644 index 0000000..cc41fa3 --- /dev/null +++ b/mdit_py_plugins/subscript/__init__.py @@ -0,0 +1,117 @@ +""" +Markdown-it-py plugin to introduce markup using ~subscript~. + +Ported from +https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs + +Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py +""" + +from __future__ import annotations + +from collections.abc import Sequence +import re + +from markdown_it import MarkdownIt +from markdown_it.renderer import RendererHTML +from markdown_it.rules_inline import StateInline +from markdown_it.token import Token +from markdown_it.utils import EnvType, OptionsDict + +__all__ = ["sub_plugin"] + +TILDE_CHAR = "~" + +WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") +UNESCAPE_RE = re.compile(r'\\([ \\!"#$%&\'()*+,.\/:;<=>?@[\]^_`{|}~-])') + + +def tokenize(state: StateInline, silent: bool) -> bool: + """Parse a ~subscript~ token.""" + start = state.pos + ch = state.src[start] + maximum = state.posMax + found = False + + # Don't run any pairs in validation mode + if silent: + return False + + if ch != TILDE_CHAR: + return False + + if start + 2 >= maximum: + return False + + state.pos = start + 1 + + while state.pos < maximum: + if state.src[state.pos] == TILDE_CHAR: + found = True + break + state.md.inline.skipToken(state) + + if not found or start + 1 == state.pos: + state.pos = start + return False + + content = state.src[start + 1 : state.pos] + + # Don't allow unescaped spaces/newlines inside + if WHITESPACE_RE.search(content) is not None: + state.pos = start + return False + + # Found a valid pair, so update posMax and pos + state.posMax = state.pos + state.pos = start + 1 + + # Earlier we checked "not silent", but this implementation does not need it + token = state.push("sub_open", "sub", 1) + token.markup = TILDE_CHAR + + token = state.push("text", "", 0) + token.content = UNESCAPE_RE.sub(r"\1", content) + + token = state.push("sub_close", "sub", -1) + token.markup = TILDE_CHAR + + state.pos = state.posMax + 1 + state.posMax = maximum + return True + + +def sub_open( + renderer: RendererHTML, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: EnvType, +) -> str: + """Render the opening tag for a ~subscript~ token.""" + return "" + + +def sub_close( + renderer: RendererHTML, + tokens: Sequence[Token], + idx: int, + options: OptionsDict, + env: EnvType, +) -> str: + """Render the closing tag for a ~subscript~ token.""" + return "" + + +def sub_plugin(md: MarkdownIt) -> None: + """ + Markdown-it-py plugin to introduce markup using ~subscript~. + + Ported from + https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs + + Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py + """ + md.inline.ruler.after("emphasis", "sub", tokenize) + md.add_render_rule("sub_open", sub_open) + md.add_render_rule("sub_close", sub_close) diff --git a/mdit_py_plugins/subscript/port.yaml b/mdit_py_plugins/subscript/port.yaml new file mode 100644 index 0000000..04cbd3f --- /dev/null +++ b/mdit_py_plugins/subscript/port.yaml @@ -0,0 +1,8 @@ +- package: markdown-it-sub + commit: 422e93885b3c611234d602aa795f3d75a62cc93e + date: 5 Dec 2023 + version: 3.0.0 + changes: + - TODO - Strikethroughs within a subscript are not rendered correctly in + markdown-it either, but that can be fixed at a later stage, perhaps + in both markdown-it and markdown-it-py diff --git a/tests/fixtures/subscript.md b/tests/fixtures/subscript.md new file mode 100644 index 0000000..14e00db --- /dev/null +++ b/tests/fixtures/subscript.md @@ -0,0 +1,130 @@ +. +~foo\~ +. +

~foo~

+. + +. +~foo bar~ +. +

~foo bar~

+. + +. +~foo\ bar\ baz~ +. +

foo bar baz

+. + +. +~\ foo\ ~ +. +

foo

+. + +. +~foo\\\\\\\ bar~ +. +

foo\\\ bar

+. + +. +~foo\\\\\\ bar~ +. +

~foo\\\ bar~

+. + +. +**~foo~ bar** +. +

foo bar

+. + + +coverage +. +*~f +. +

*~f

+. + +Basic: +. +H~2~O +. +

H2O

+. + +Spaces: +. +H~2 O~2 +. +

H~2 O~2

+. + +Escaped: +. +H\~2\~O +. +

H~2~O

+. + +Nested: +. +a~b~c~d~e +. +

abcde

+. + +Strikethrough versus subscript: +. +~~strikethrough~~ versus ~subscript~ +. +

strikethrough versus subscript

+. + +Subscript in strikethrough (beginning): +. +~~~subscript~ in the beginning within a strikethrough is perceived as first line of a code block and hence ignored~~ +. +
+. + +Strikethrough in subscript (beginning): +. +~~~strikethrough~ in the beginning within a subscript is perceived as first line of a code block and hence ignored~~ +. +
+. + +Subscript in strikethrough (end): +. +~~strikethrough contains ~subscript~~~ +. +

strikethrough contains subscript

+. + +Strikethrough in subscript (end): +. +~subscript contains ~~strikethrough~~~ +TODO: This is not rendered correctly in markdown-it either, but can be fixed +. +

~subscript contains strikethrough~ +TODO: This is not rendered correctly in markdown-it either, but can be fixed

+. + +Subscript in strikethrough: +. +~~strikethrough with ~subscript~ text~~ +. +

strikethrough with subscript text

+. + +Strikethrough in subscript: +. +~subscript contains ~~strikethrough~~ text~ +TODO: This is not rendered correctly in markdown-it either, but can be fixed +. +

~subscript contains strikethrough text~ +TODO: This is not rendered correctly in markdown-it either, but can be fixed

+. \ No newline at end of file diff --git a/tests/test_subscript.py b/tests/test_subscript.py new file mode 100644 index 0000000..699beaf --- /dev/null +++ b/tests/test_subscript.py @@ -0,0 +1,23 @@ +"""Tests for subscript plugin.""" + +from pathlib import Path + +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file +import pytest + +from mdit_py_plugins.subscript import sub_plugin + +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "subscript.md") + + +@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH)) +def test_all(line, title, input, expected): + """Tests for subscript plugin.""" + md = MarkdownIt("commonmark").enable("strikethrough").use(sub_plugin) + text = md.render(input) + try: + assert text.rstrip() == expected.rstrip() + except AssertionError: + print(text) + raise