Skip to content

Commit 39c681f

Browse files
elijahgreensteinpre-commit-ci[bot]KyleKingchrisjsewell
authored
✨ NEW: Add superscript plugin and tests (#128)
Add a superscript plugin and associated tests ported from <https://github.com/markdown-it/markdown-it-sup>. Update documentation with superscript plugin and remove superscript (and subscript, added in PR <#122>) from list of plugins to port. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kyle King <KyleKing@users.noreply.github.com> Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>
1 parent 62f0058 commit 39c681f

File tree

7 files changed

+198
-6
lines changed

7 files changed

+198
-6
lines changed

docs/index.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ html_string = md.render("some *Markdown*")
119119
.. autofunction:: mdit_py_plugins.subscript.sub_plugin
120120
```
121121

122+
## Superscript
123+
124+
```{eval-rst}
125+
.. autofunction:: mdit_py_plugins.superscript.superscript_plugin
126+
```
127+
122128
## MyST plugins
123129

124130
`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)
@@ -134,8 +140,6 @@ Use the `mdit_py_plugins` as a guide to write your own, following the [markdown-
134140

135141
There are many other plugins which could easily be ported from the JS versions (and hopefully will):
136142

137-
- [subscript](https://github.com/markdown-it/markdown-it-sub)
138-
- [superscript](https://github.com/markdown-it/markdown-it-sup)
139143
- [abbreviation](https://github.com/markdown-it/markdown-it-abbr)
140144
- [emoji](https://github.com/markdown-it/markdown-it-emoji)
141145
- [insert](https://github.com/markdown-it/markdown-it-ins)

mdit_py_plugins/subscript/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@
1010
from __future__ import annotations
1111

1212
from collections.abc import Sequence
13-
import re
1413

1514
from markdown_it import MarkdownIt
1615
from markdown_it.renderer import RendererHTML
1716
from markdown_it.rules_inline import StateInline
1817
from markdown_it.token import Token
1918
from markdown_it.utils import EnvType, OptionsDict
2019

20+
from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE
21+
2122
__all__ = ["sub_plugin"]
2223

2324
TILDE_CHAR = "~"
2425

25-
WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s")
26-
UNESCAPE_RE = re.compile(r'\\([ \\!"#$%&\'()*+,.\/:;<=>?@[\]^_`{|}~-])')
27-
2826

2927
def tokenize(state: StateInline, silent: bool) -> bool:
3028
"""Parse a ~subscript~ token."""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Superscript tag plugin, ported from Markdown-It."""
2+
3+
from .index import superscript_plugin
4+
5+
__all__ = ("superscript_plugin",)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Superscript tag plugin.
2+
3+
Ported by Elijah Greenstein from https://github.com/markdown-it/markdown-it-sup
4+
cf. Subscript tag plugin, https://mdit-py-plugins.readthedocs.io/en/latest/#subscripts
5+
6+
MIT License
7+
Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin.
8+
9+
Permission is hereby granted, free of charge, to any person
10+
obtaining a copy of this software and associated documentation
11+
files (the "Software"), to deal in the Software without
12+
restriction, including without limitation the rights to use,
13+
copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the
15+
Software is furnished to do so, subject to the following
16+
conditions:
17+
18+
The above copyright notice and this permission notice shall be
19+
included in all copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28+
OTHER DEALINGS IN THE SOFTWARE.
29+
"""
30+
31+
from markdown_it import MarkdownIt
32+
from markdown_it.rules_inline import StateInline
33+
34+
from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE
35+
36+
37+
def superscript_plugin(md: MarkdownIt) -> None:
38+
"""Superscript (``<sup>``) tag plugin for Markdown-It-Py.
39+
40+
This plugin is ported from `markdown-it-sup
41+
<https://github.com/markdown-it/markdown-it-sup>`_. Markup is based on the
42+
`Pandoc superscript extension
43+
<https://pandoc.org/MANUAL.html#superscripts-and-subscripts>`_.
44+
45+
Place superscripted text within caret ``^`` characters. You must escape any
46+
spaces in the superscripted text. Note that you cannot use newline or tab
47+
characters, and that nested markup is not supported.
48+
49+
Example usage:
50+
51+
>>> from markdown_it import MarkdownIt
52+
>>> from mdit_py_plugins.superscript import superscript_plugin
53+
>>> md = MarkdownIt().use(superscript_plugin)
54+
>>> md.render("1^st^")
55+
'<p>1<sup>st</sup></p>\\n'
56+
>>> md.render("this^text\\\\ has\\\\ spaces^")
57+
'<p>this<sup>text has spaces</sup></p>\\n'
58+
"""
59+
60+
def superscript(state: StateInline, silent: bool) -> bool:
61+
"""Parse inline text for superscripted text between caret ``^`` characters."""
62+
maximum = state.posMax
63+
start = state.pos
64+
65+
if ord(state.src[start]) != 0x5E: # Check if char is `^`
66+
return False
67+
if silent: # Do not run any pairs in validation mode
68+
return False
69+
if start + 2 >= maximum:
70+
return False
71+
72+
state.pos = start + 1
73+
found = False
74+
75+
while state.pos < maximum:
76+
if ord(state.src[state.pos]) == 0x5E: # Check if char is `^`
77+
found = True
78+
break
79+
state.md.inline.skipToken(state)
80+
81+
if (not found) or (start + 1 == state.pos):
82+
state.pos = start
83+
return False
84+
85+
content = state.src[start + 1 : state.pos]
86+
87+
# Do not allow unescaped spaces/newlines inside
88+
if WHITESPACE_RE.search(content) is not None:
89+
state.pos = start
90+
return False
91+
92+
# Found!
93+
state.posMax = state.pos
94+
state.pos = start + 1
95+
96+
# Earlier we checked !silent, but this implementation does not need it
97+
token_so = state.push("sup_open", "sup", 1)
98+
token_so.markup = "^"
99+
100+
token_t = state.push("text", "", 0)
101+
token_t.content = UNESCAPE_RE.sub(r"\1", content)
102+
103+
token_sc = state.push("sup_close", "sup", -1)
104+
token_sc.markup = "^"
105+
106+
state.pos = state.posMax + 1
107+
state.posMax = maximum
108+
return True
109+
110+
md.inline.ruler.after("emphasis", "sup", superscript)

mdit_py_plugins/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from markdown_it.rules_block import StateBlock
24

35

@@ -10,3 +12,8 @@ def is_code_block(state: StateBlock, line: int) -> bool:
1012
pass
1113

1214
return (state.sCount[line] - state.blkIndent) >= 4
15+
16+
17+
# Regex for subscript and superscript plugins
18+
UNESCAPE_RE = re.compile(r"\\([ \\!\"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])")
19+
WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s")

tests/fixtures/superscript.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.
2+
^test^
3+
.
4+
<p><sup>test</sup></p>
5+
.
6+
7+
.
8+
^foo\^
9+
.
10+
<p>^foo^</p>
11+
.
12+
13+
.
14+
2^4 + 3^5
15+
.
16+
<p>2^4 + 3^5</p>
17+
.
18+
19+
.
20+
^foo~bar^baz^bar~foo^
21+
.
22+
<p><sup>foo~bar</sup>baz<sup>bar~foo</sup></p>
23+
.
24+
25+
.
26+
^\ foo\ ^
27+
.
28+
<p><sup> foo </sup></p>
29+
.
30+
31+
.
32+
^foo\\\\\\\ bar^
33+
.
34+
<p><sup>foo\\\ bar</sup></p>
35+
.
36+
37+
.
38+
^foo\\\\\\ bar^
39+
.
40+
<p>^foo\\\ bar^</p>
41+
.
42+
43+
.
44+
**^foo^ bar**
45+
.
46+
<p><strong><sup>foo</sup> bar</strong></p>
47+
.
48+

tests/test_superscript.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
3+
from markdown_it import MarkdownIt
4+
from markdown_it.utils import read_fixture_file
5+
import pytest
6+
7+
from mdit_py_plugins.superscript import superscript_plugin
8+
9+
FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
10+
11+
12+
@pytest.mark.parametrize(
13+
"line,title,input,expected",
14+
read_fixture_file(FIXTURE_PATH.joinpath("superscript.md")),
15+
)
16+
def test_superscript_fixtures(line, title, input, expected):
17+
md = MarkdownIt("commonmark").use(superscript_plugin)
18+
md.options["xhtmlOut"] = False
19+
text = md.render(input)
20+
assert text.rstrip() == expected.rstrip()

0 commit comments

Comments
 (0)