-
Notifications
You must be signed in to change notification settings - Fork 37
✨ NEW: Add superscript plugin and tests #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
chrisjsewell
merged 11 commits into
executablebooks:master
from
elijahgreenstein:superscript
Mar 2, 2026
Merged
Changes from 5 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e0dc45c
Add superscript plugin
elijahgreenstein 7f809b6
Add tests for superscript plugin
elijahgreenstein 5ccae1d
Add superscript plugin to documentation
elijahgreenstein 02e0fd2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e2fb671
Remove unnecessary import
elijahgreenstein fe97ca2
Remove unnecessary print
elijahgreenstein 7e38cee
Remove unnecessary title check
elijahgreenstein 76fbdf9
Move shared sub/superscript regex to utility file
elijahgreenstein 822a097
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 75df4e9
Correct docstring regarding use of spaces
elijahgreenstein b982371
Merge branch 'master' into superscript
chrisjsewell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Superscript tag plugin, ported from Markdown-It.""" | ||
|
|
||
| from .index import superscript_plugin | ||
|
|
||
| __all__ = ("superscript_plugin",) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| """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. | ||
| """ | ||
|
|
||
| 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") | ||
|
|
||
|
|
||
| def superscript_plugin(md: MarkdownIt) -> None: | ||
| """Superscript (``<sup>``) tag plugin for Markdown-It-Py. | ||
|
|
||
| This plugin is ported from `markdown-it-sup <https://github.com/markdown-it/markdown-it-sup>`_. Markup is based on the `Pandoc superscript extension <https://pandoc.org/MANUAL.html#superscripts-and-subscripts>`_. | ||
|
|
||
| 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^") | ||
| '<p>1<sup>st</sup></p>\\n' | ||
| >>> md.render("2^nd^") | ||
| '<p>2<sup>nd</sup></p>\\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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| . | ||
| ^test^ | ||
| . | ||
| <p><sup>test</sup></p> | ||
| . | ||
|
|
||
| . | ||
| ^foo\^ | ||
| . | ||
| <p>^foo^</p> | ||
| . | ||
|
|
||
| . | ||
| 2^4 + 3^5 | ||
| . | ||
| <p>2^4 + 3^5</p> | ||
| . | ||
|
|
||
| . | ||
| ^foo~bar^baz^bar~foo^ | ||
| . | ||
| <p><sup>foo~bar</sup>baz<sup>bar~foo</sup></p> | ||
| . | ||
|
|
||
| . | ||
| ^\ foo\ ^ | ||
| . | ||
| <p><sup> foo </sup></p> | ||
| . | ||
|
|
||
| . | ||
| ^foo\\\\\\\ bar^ | ||
| . | ||
| <p><sup>foo\\\ bar</sup></p> | ||
| . | ||
|
|
||
| . | ||
| ^foo\\\\\\ bar^ | ||
| . | ||
| <p>^foo\\\ bar^</p> | ||
| . | ||
|
|
||
| . | ||
| **^foo^ bar** | ||
| . | ||
| <p><strong><sup>foo</sup> bar</strong></p> | ||
| . | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
|
elijahgreenstein marked this conversation as resolved.
Outdated
|
||
| md.options["xhtmlOut"] = False | ||
| text = md.render(input) | ||
| print(text) | ||
|
elijahgreenstein marked this conversation as resolved.
Outdated
|
||
| assert text.rstrip() == expected.rstrip() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While
WHITESPACE_REis the same, I'm not sure ifUNESCAPE_REshould diverge the way it does and maybe they could be standardized and extracted into a utility file?https://github.com/elijahgreenstein/mdit-py-plugins/blob/e2fb671b3a66e86690239137429d5ef69f5e06d5/mdit_py_plugins/subscript/__init__.py#L25-L26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there, thanks for the help cleaning up the tests! As for
UNESCAPE_RE, I checked the original JS subscript plugin and the original JS superscript plugin, and it looks like the Python port of the subscript plugin here (inmdit_py_plugins) included an extra backslash in the middle of theUNESCAPE_REstring. As you suggested, I've standardized the expressions for both plugins and moved them into the existing utility file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!