Skip to content

Commit 91264a6

Browse files
authored
✨ NEW: Add plugin & tests to render subscripts (#122)
1 parent 2236898 commit 91264a6

File tree

6 files changed

+317
-0
lines changed

6 files changed

+317
-0
lines changed

docs/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ html_string = md.render("some *Markdown*")
113113
.. autofunction:: mdit_py_plugins.amsmath.amsmath_plugin
114114
```
115115

116+
## Subscripts
117+
118+
```{eval-rst}
119+
.. autofunction:: mdit_py_plugins.subscript.sub_plugin
120+
```
121+
116122
## MyST plugins
117123

118124
`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)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Markdown-it-py plugin to introduce <sub> markup using ~subscript~.
3+
4+
Ported from
5+
https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs
6+
7+
Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from collections.abc import Sequence
13+
import re
14+
15+
from markdown_it import MarkdownIt
16+
from markdown_it.renderer import RendererHTML
17+
from markdown_it.rules_inline import StateInline
18+
from markdown_it.token import Token
19+
from markdown_it.utils import EnvType, OptionsDict
20+
21+
__all__ = ["sub_plugin"]
22+
23+
TILDE_CHAR = "~"
24+
25+
WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s")
26+
UNESCAPE_RE = re.compile(r'\\([ \\!"#$%&\'()*+,.\/:;<=>?@[\]^_`{|}~-])')
27+
28+
29+
def tokenize(state: StateInline, silent: bool) -> bool:
30+
"""Parse a ~subscript~ token."""
31+
start = state.pos
32+
ch = state.src[start]
33+
maximum = state.posMax
34+
found = False
35+
36+
# Don't run any pairs in validation mode
37+
if silent:
38+
return False
39+
40+
if ch != TILDE_CHAR:
41+
return False
42+
43+
if start + 2 >= maximum:
44+
return False
45+
46+
state.pos = start + 1
47+
48+
while state.pos < maximum:
49+
if state.src[state.pos] == TILDE_CHAR:
50+
found = True
51+
break
52+
state.md.inline.skipToken(state)
53+
54+
if not found or start + 1 == state.pos:
55+
state.pos = start
56+
return False
57+
58+
content = state.src[start + 1 : state.pos]
59+
60+
# Don't allow unescaped spaces/newlines inside
61+
if WHITESPACE_RE.search(content) is not None:
62+
state.pos = start
63+
return False
64+
65+
# Found a valid pair, so update posMax and pos
66+
state.posMax = state.pos
67+
state.pos = start + 1
68+
69+
# Earlier we checked "not silent", but this implementation does not need it
70+
token = state.push("sub_open", "sub", 1)
71+
token.markup = TILDE_CHAR
72+
73+
token = state.push("text", "", 0)
74+
token.content = UNESCAPE_RE.sub(r"\1", content)
75+
76+
token = state.push("sub_close", "sub", -1)
77+
token.markup = TILDE_CHAR
78+
79+
state.pos = state.posMax + 1
80+
state.posMax = maximum
81+
return True
82+
83+
84+
def sub_open(
85+
renderer: RendererHTML,
86+
tokens: Sequence[Token],
87+
idx: int,
88+
options: OptionsDict,
89+
env: EnvType,
90+
) -> str:
91+
"""Render the opening tag for a ~subscript~ token."""
92+
return "<sub>"
93+
94+
95+
def sub_close(
96+
renderer: RendererHTML,
97+
tokens: Sequence[Token],
98+
idx: int,
99+
options: OptionsDict,
100+
env: EnvType,
101+
) -> str:
102+
"""Render the closing tag for a ~subscript~ token."""
103+
return "</sub>"
104+
105+
106+
def sub_plugin(md: MarkdownIt) -> None:
107+
"""
108+
Markdown-it-py plugin to introduce <sub> markup using ~subscript~.
109+
110+
Ported from
111+
https://github.com/markdown-it/markdown-it-sub/blob/master/index.mjs
112+
113+
Originally ported during implementation of https://github.com/hasgeek/funnel/blob/main/funnel/utils/markdown/mdit_plugins/sub_tag.py
114+
"""
115+
md.inline.ruler.after("emphasis", "sub", tokenize)
116+
md.add_render_rule("sub_open", sub_open)
117+
md.add_render_rule("sub_close", sub_close)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
- package: markdown-it-sub
2+
commit: 422e93885b3c611234d602aa795f3d75a62cc93e
3+
date: 5 Dec 2023
4+
version: 3.0.0
5+
changes:
6+
- TODO - Some strikethrough and subscript combinations are not rendered
7+
correctly in markdown-it either, but that can be fixed at a later stage,
8+
perhaps in both markdown-it and markdown-it-py.
9+
See `tests/fixtures/subscript_strikethrough.md` for examples.

tests/fixtures/subscript.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
.
2+
~foo\~
3+
.
4+
<p>~foo~</p>
5+
.
6+
7+
.
8+
~foo bar~
9+
.
10+
<p>~foo bar~</p>
11+
.
12+
13+
.
14+
~foo\ bar\ baz~
15+
.
16+
<p><sub>foo bar baz</sub></p>
17+
.
18+
19+
.
20+
~\ foo\ ~
21+
.
22+
<p><sub> foo </sub></p>
23+
.
24+
25+
.
26+
~foo\\\\\\\ bar~
27+
.
28+
<p><sub>foo\\\ bar</sub></p>
29+
.
30+
31+
.
32+
~foo\\\\\\ bar~
33+
.
34+
<p>~foo\\\ bar~</p>
35+
.
36+
37+
.
38+
**~foo~ bar**
39+
.
40+
<p><strong><sub>foo</sub> bar</strong></p>
41+
.
42+
43+
44+
coverage
45+
.
46+
*~f
47+
.
48+
<p>*~f</p>
49+
.
50+
51+
Basic:
52+
.
53+
H~2~O
54+
.
55+
<p>H<sub>2</sub>O</p>
56+
.
57+
58+
Spaces:
59+
.
60+
H~2 O~2
61+
.
62+
<p>H~2 O~2</p>
63+
.
64+
65+
Escaped:
66+
.
67+
H\~2\~O
68+
.
69+
<p>H~2~O</p>
70+
.
71+
72+
Nested:
73+
.
74+
a~b~c~d~e
75+
.
76+
<p>a<sub>b</sub>c<sub>d</sub>e</p>
77+
.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
Strikethrough versus subscript:
2+
.
3+
~~strikethrough~~versus~subscript~
4+
.
5+
<p><s>strikethrough</s>versus<sub>subscript</sub></p>
6+
.
7+
8+
Subscript in strikethrough (beginning):
9+
.
10+
~~~subscript~strikethrough~~
11+
This ends up being rendered as a code block, but that's expected.
12+
Hence, it has to be closed with `~~~`
13+
~~~
14+
Only then will the following text be rendered as it is intended.
15+
We cannot use `~~~subscript~strikethrough~~` at the beginning of a line.
16+
.
17+
<pre><code class="language-subscript~strikethrough~~">This ends up being rendered as a code block, but that's expected.
18+
Hence, it has to be closed with `~~~`
19+
</code></pre>
20+
<p>Only then will the following text be rendered as it is intended.
21+
We cannot use <code>~~~subscript~strikethrough~~</code> at the beginning of a line.</p>
22+
.
23+
24+
Strikethrough in subscript (beginning):
25+
.
26+
~~~strikethrough~~subscript~
27+
This ends up being rendered as a code block, but that's expected.
28+
Hence, it has to be closed with `~~~`
29+
~~~
30+
Only then will the following text be rendered as it is intended.
31+
We cannot use `~~~strikethrough~~subscript~` at the beginning of a line.
32+
.
33+
<pre><code class="language-strikethrough~~subscript~">This ends up being rendered as a code block, but that's expected.
34+
Hence, it has to be closed with `~~~`
35+
</code></pre>
36+
<p>Only then will the following text be rendered as it is intended.
37+
We cannot use <code>~~~strikethrough~~subscript~</code> at the beginning of a line.</p>
38+
.
39+
40+
Subscript in strikethrough (end):
41+
.
42+
~~strikethrough~subscript~~~
43+
.
44+
<p><s>strikethrough<sub>subscript</sub></s></p>
45+
.
46+
47+
Strikethrough in subscript (end):
48+
.
49+
TODO: ~subscript~~strikethrough~~~
50+
.
51+
<p>TODO: <sub>subscript</sub><sub>strikethrough</sub>~~</p>
52+
.
53+
54+
Subscript in strikethrough:
55+
.
56+
~~strikethrough~subscript~strikethrough~~
57+
.
58+
<p><s>strikethrough<sub>subscript</sub>strikethrough</s></p>
59+
.
60+
61+
Strikethrough in subscript:
62+
.
63+
TODO: ~subscript~~strikethrough~~subscript~
64+
This should have beeen similar to *emphasised**strong**emphasised*.
65+
.
66+
<p>TODO: <sub>subscript</sub><sub>strikethrough</sub><sub>subscript</sub>
67+
This should have beeen similar to <em>emphasised<strong>strong</strong>emphasised</em>.</p>
68+
.

tests/test_subscript.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Tests for subscript plugin."""
2+
3+
from pathlib import Path
4+
5+
from markdown_it import MarkdownIt
6+
from markdown_it.utils import read_fixture_file
7+
import pytest
8+
9+
from mdit_py_plugins.subscript import sub_plugin
10+
11+
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "subscript.md")
12+
STRIKETHROUGH_FIXTURE_PATH = Path(__file__).parent.joinpath(
13+
"fixtures", "subscript_strikethrough.md"
14+
)
15+
16+
17+
@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH))
18+
def test_all(line, title, input, expected):
19+
"""Tests for subscript plugin."""
20+
md = MarkdownIt("commonmark").use(sub_plugin)
21+
text = md.render(input)
22+
try:
23+
assert text.rstrip() == expected.rstrip()
24+
except AssertionError:
25+
print(text)
26+
raise
27+
28+
29+
@pytest.mark.parametrize(
30+
"line,title,input,expected", read_fixture_file(STRIKETHROUGH_FIXTURE_PATH)
31+
)
32+
def test_all_strikethrough(line, title, input, expected):
33+
"""Tests for subscript plugin with strikethrough enabled."""
34+
md = MarkdownIt("commonmark").enable("strikethrough").use(sub_plugin)
35+
text = md.render(input)
36+
try:
37+
assert text.rstrip() == expected.rstrip()
38+
except AssertionError:
39+
print(text)
40+
raise

0 commit comments

Comments
 (0)