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) 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..274325a --- /dev/null +++ b/mdit_py_plugins/superscript/index.py @@ -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 (````) 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) 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()