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 1/5] 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 2/5] 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 3/5] 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 4/5] [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 5/5] 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