From e0dc45cdc93dff418f996f3f38319bba6457e98e Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:56:15 -0700 Subject: [PATCH 01/10] Add superscript plugin Plugin ported from https://github.com/markdown-it/markdown-it-sup --- mdit_py_plugins/superscript/__init__.py | 5 ++ mdit_py_plugins/superscript/index.py | 112 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 mdit_py_plugins/superscript/__init__.py create mode 100644 mdit_py_plugins/superscript/index.py diff --git a/mdit_py_plugins/superscript/__init__.py b/mdit_py_plugins/superscript/__init__.py new file mode 100644 index 0000000..063e4a2 --- /dev/null +++ b/mdit_py_plugins/superscript/__init__.py @@ -0,0 +1,5 @@ +"""Superscript tag plugin, ported from Markdown-It.""" + +from .index import superscript_plugin + +__all__ = ("superscript_plugin",) diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py new file mode 100644 index 0000000..a2320d4 --- /dev/null +++ b/mdit_py_plugins/superscript/index.py @@ -0,0 +1,112 @@ +"""Superscript tag plugin. + +Ported by Elijah Greenstein from https://github.com/markdown-it/markdown-it-sup +cf. Subscript tag plugin, https://mdit-py-plugins.readthedocs.io/en/latest/#subscripts + +MIT License +Copyright (c) 2014-2015 Vitaly Puzrin, Alex Kocharin. + +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. +from __future__ import annotations +""" + +from collections.abc import Sequence +import re +from typing import TYPE_CHECKING + +from markdown_it import MarkdownIt +from markdown_it.rules_inline import StateInline + + +UNESCAPE_RE = re.compile(r"\\([ \\!\"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])") +WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") + + +def superscript_plugin(md: MarkdownIt) -> None: + """Superscript (````) tag plugin for Markdown-It-Py. + + This plugin is ported from `markdown-it-sup `_. Markup is based on the `Pandoc superscript extension `_. + + Surround superscripted text with caret ``^`` characters. Superscripted text cannot contain whitespace characters. Nested markup is not supported. + + Example usage: + + >>> from markdown_it import MarkdownIt + >>> from mdit_py_plugins.superscript import superscript_plugin + >>> md = MarkdownIt().use(superscript_plugin) + >>> md.render("1^st^") + '

1st

\\n' + >>> md.render("2^nd^") + '

2nd

\\n' + """ + + def superscript(state: StateInline, silent: bool) -> bool: + """Parse inline text for superscripted text between caret ``^`` characters.""" + maximum = state.posMax + start = state.pos + + if ord(state.src[start]) != 0x5E: # Check if char is `^` + return False + if silent: # Do not run any pairs in validation mode + return False + if start + 2 >= maximum: + return False + + state.pos = start + 1 + found = False + + while state.pos < maximum: + if ord(state.src[state.pos]) == 0x5E: # Check if char is `^` + 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] + + # Do not allow unescaped spaces/newlines inside + if WHITESPACE_RE.search(content) is not None: + state.pos = start + return False + + # Found! + state.posMax = state.pos + state.pos = start + 1 + + # Earlier we checked !silent, but this implementation does not need it + token_so = state.push("sup_open", "sup", 1) + token_so.markup = "^" + + token_t = state.push("text", "", 0) + token_t.content = UNESCAPE_RE.sub(r"\1", content) + + token_sc = state.push("sup_close", "sup", -1) + token_sc.markup = "^" + + state.pos = state.posMax + 1 + state.posMax = maximum + return True + + md.inline.ruler.after("emphasis", "sup", superscript) From 7f809b618036dbfde33f739a77cff92e86827429 Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:59:45 -0700 Subject: [PATCH 02/10] Add tests for superscript plugin Fixtures file is copied from the `markdown-it-sup` tests: https://github.com/markdown-it/markdown-it-sup/blob/master/test/fixtures/sup.txt Test is modelled on other Markdown-It-Py inline tests. --- tests/fixtures/superscript.md | 48 +++++++++++++++++++++++++++++++++++ tests/test_superscript.py | 23 +++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/fixtures/superscript.md create mode 100644 tests/test_superscript.py diff --git a/tests/fixtures/superscript.md b/tests/fixtures/superscript.md new file mode 100644 index 0000000..37e1cef --- /dev/null +++ b/tests/fixtures/superscript.md @@ -0,0 +1,48 @@ +. +^test^ +. +

test

+. + +. +^foo\^ +. +

^foo^

+. + +. +2^4 + 3^5 +. +

2^4 + 3^5

+. + +. +^foo~bar^baz^bar~foo^ +. +

foo~barbazbar~foo

+. + +. +^\ foo\ ^ +. +

foo

+. + +. +^foo\\\\\\\ bar^ +. +

foo\\\ bar

+. + +. +^foo\\\\\\ bar^ +. +

^foo\\\ bar^

+. + +. +**^foo^ bar** +. +

foo bar

+. + diff --git a/tests/test_superscript.py b/tests/test_superscript.py new file mode 100644 index 0000000..957112b --- /dev/null +++ b/tests/test_superscript.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file +import pytest + +from mdit_py_plugins.superscript import superscript_plugin + +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") + + +@pytest.mark.parametrize( + "line,title,input,expected", + read_fixture_file(FIXTURE_PATH.joinpath("superscript.md")), +) +def test_superscript_fixtures(line, title, input, expected): + md = MarkdownIt("commonmark").use(superscript_plugin) + if "DISABLE-CODEBLOCKS" in title: + md.disable("code") + md.options["xhtmlOut"] = False + text = md.render(input) + print(text) + assert text.rstrip() == expected.rstrip() From 5ccae1d6d61d96191ecd49769ed69d169f53cbc4 Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:01:39 -0700 Subject: [PATCH 03/10] Add superscript plugin to documentation Also, remove superscript (and subscript) from list of plugins to port. --- docs/index.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index ea43f45..8b6898c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -119,6 +119,12 @@ html_string = md.render("some *Markdown*") .. autofunction:: mdit_py_plugins.subscript.sub_plugin ``` +## Superscript + +```{eval-rst} +.. autofunction:: mdit_py_plugins.superscript.superscript_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) @@ -134,8 +140,6 @@ Use the `mdit_py_plugins` as a guide to write your own, following the [markdown- There are many other plugins which could easily be ported from the JS versions (and hopefully will): -- [subscript](https://github.com/markdown-it/markdown-it-sub) -- [superscript](https://github.com/markdown-it/markdown-it-sup) - [abbreviation](https://github.com/markdown-it/markdown-it-abbr) - [emoji](https://github.com/markdown-it/markdown-it-emoji) - [insert](https://github.com/markdown-it/markdown-it-ins) From 02e0fd26c158873ae206177acb7639dabbe0509d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 04:09:45 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mdit_py_plugins/superscript/index.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py index a2320d4..51070f8 100644 --- a/mdit_py_plugins/superscript/index.py +++ b/mdit_py_plugins/superscript/index.py @@ -29,14 +29,11 @@ from __future__ import annotations """ -from collections.abc import Sequence import re -from typing import TYPE_CHECKING from markdown_it import MarkdownIt from markdown_it.rules_inline import StateInline - UNESCAPE_RE = re.compile(r"\\([ \\!\"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])") WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") From e2fb671b3a66e86690239137429d5ef69f5e06d5 Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:22:11 -0700 Subject: [PATCH 05/10] Remove unnecessary import --- mdit_py_plugins/superscript/index.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py index 51070f8..274325a 100644 --- a/mdit_py_plugins/superscript/index.py +++ b/mdit_py_plugins/superscript/index.py @@ -26,7 +26,6 @@ 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. -from __future__ import annotations """ import re From fe97ca2815a412c9dc8f30934d986c131bcecf01 Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:27:54 -0800 Subject: [PATCH 06/10] Remove unnecessary print Co-authored-by: Kyle King --- tests/test_superscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_superscript.py b/tests/test_superscript.py index 957112b..2513dd4 100644 --- a/tests/test_superscript.py +++ b/tests/test_superscript.py @@ -19,5 +19,4 @@ def test_superscript_fixtures(line, title, input, expected): md.disable("code") md.options["xhtmlOut"] = False text = md.render(input) - print(text) assert text.rstrip() == expected.rstrip() From 7e38cee939c5668434af31d4d5d9388d6acf89ff Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:28:21 -0800 Subject: [PATCH 07/10] Remove unnecessary title check Co-authored-by: Kyle King --- tests/test_superscript.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_superscript.py b/tests/test_superscript.py index 2513dd4..9ad0636 100644 --- a/tests/test_superscript.py +++ b/tests/test_superscript.py @@ -15,8 +15,6 @@ ) def test_superscript_fixtures(line, title, input, expected): md = MarkdownIt("commonmark").use(superscript_plugin) - if "DISABLE-CODEBLOCKS" in title: - md.disable("code") md.options["xhtmlOut"] = False text = md.render(input) assert text.rstrip() == expected.rstrip() From 76fbdf93ad9c33418bcf352fa23600a662cb105f Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:46:05 -0800 Subject: [PATCH 08/10] Move shared sub/superscript regex to utility file The subscript and superscript plugins both use the same regular expressions. This commit standardizes and centralizes the regex into a utility file. --- mdit_py_plugins/subscript/__init__.py | 5 +---- mdit_py_plugins/superscript/index.py | 6 +----- mdit_py_plugins/utils.py | 7 +++++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mdit_py_plugins/subscript/__init__.py b/mdit_py_plugins/subscript/__init__.py index cc41fa3..cafc344 100644 --- a/mdit_py_plugins/subscript/__init__.py +++ b/mdit_py_plugins/subscript/__init__.py @@ -10,21 +10,18 @@ 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 +from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE __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.""" diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py index 274325a..2516fb1 100644 --- a/mdit_py_plugins/superscript/index.py +++ b/mdit_py_plugins/superscript/index.py @@ -28,13 +28,9 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import re - from markdown_it import MarkdownIt from markdown_it.rules_inline import StateInline - -UNESCAPE_RE = re.compile(r"\\([ \\!\"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])") -WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") +from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE def superscript_plugin(md: MarkdownIt) -> None: diff --git a/mdit_py_plugins/utils.py b/mdit_py_plugins/utils.py index bd8db4e..6c08ce4 100644 --- a/mdit_py_plugins/utils.py +++ b/mdit_py_plugins/utils.py @@ -1,3 +1,5 @@ +import re + from markdown_it.rules_block import StateBlock @@ -10,3 +12,8 @@ def is_code_block(state: StateBlock, line: int) -> bool: pass return (state.sCount[line] - state.blkIndent) >= 4 + + +# Regex for subscript and superscript plugins +UNESCAPE_RE = re.compile(r"\\([ \\!\"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])") +WHITESPACE_RE = re.compile(r"(^|[^\\])(\\\\)*\s") From 822a097bda8e02ccf6bae26cb5ae1cc37a885619 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:48:52 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mdit_py_plugins/subscript/__init__.py | 1 + mdit_py_plugins/superscript/index.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mdit_py_plugins/subscript/__init__.py b/mdit_py_plugins/subscript/__init__.py index cafc344..2f79944 100644 --- a/mdit_py_plugins/subscript/__init__.py +++ b/mdit_py_plugins/subscript/__init__.py @@ -16,6 +16,7 @@ from markdown_it.rules_inline import StateInline from markdown_it.token import Token from markdown_it.utils import EnvType, OptionsDict + from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE __all__ = ["sub_plugin"] diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py index 2516fb1..1473c48 100644 --- a/mdit_py_plugins/superscript/index.py +++ b/mdit_py_plugins/superscript/index.py @@ -30,6 +30,7 @@ from markdown_it import MarkdownIt from markdown_it.rules_inline import StateInline + from mdit_py_plugins.utils import UNESCAPE_RE, WHITESPACE_RE From 75df4e9c7ea090911dbb61bef47b7264e6c5cf29 Mon Sep 17 00:00:00 2001 From: Elijah Greenstein <197816462+elijahgreenstein@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:05:02 -0800 Subject: [PATCH 10/10] Correct docstring regarding use of spaces My original docstring stated that superscripted text cannot contain any whitespace characters. In fact, superscripted text can contain space characters, as long as they are escaped. --- mdit_py_plugins/superscript/index.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mdit_py_plugins/superscript/index.py b/mdit_py_plugins/superscript/index.py index 1473c48..5e14bba 100644 --- a/mdit_py_plugins/superscript/index.py +++ b/mdit_py_plugins/superscript/index.py @@ -37,9 +37,14 @@ def superscript_plugin(md: MarkdownIt) -> None: """Superscript (````) tag plugin for Markdown-It-Py. - This plugin is ported from `markdown-it-sup `_. Markup is based on the `Pandoc superscript extension `_. + This plugin is ported from `markdown-it-sup + `_. Markup is based on the + `Pandoc superscript extension + `_. - Surround superscripted text with caret ``^`` characters. Superscripted text cannot contain whitespace characters. Nested markup is not supported. + Place superscripted text within caret ``^`` characters. You must escape any + spaces in the superscripted text. Note that you cannot use newline or tab + characters, and that nested markup is not supported. Example usage: @@ -48,8 +53,8 @@ def superscript_plugin(md: MarkdownIt) -> None: >>> md = MarkdownIt().use(superscript_plugin) >>> md.render("1^st^") '

1st

\\n' - >>> md.render("2^nd^") - '

2nd

\\n' + >>> md.render("this^text\\\\ has\\\\ spaces^") + '

thistext has spaces

\\n' """ def superscript(state: StateInline, silent: bool) -> bool: