summaryrefslogtreecommitdiff
path: root/src/mistune/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/mistune/plugins')
-rw-r--r--src/mistune/plugins/__init__.py38
-rw-r--r--src/mistune/plugins/abbr.py103
-rw-r--r--src/mistune/plugins/def_list.py135
-rw-r--r--src/mistune/plugins/footnotes.py153
-rw-r--r--src/mistune/plugins/formatting.py173
-rw-r--r--src/mistune/plugins/math.py57
-rw-r--r--src/mistune/plugins/ruby.py100
-rw-r--r--src/mistune/plugins/speedup.py44
-rw-r--r--src/mistune/plugins/spoiler.py80
-rw-r--r--src/mistune/plugins/table.py179
-rw-r--r--src/mistune/plugins/task_lists.py67
-rw-r--r--src/mistune/plugins/url.py23
12 files changed, 1152 insertions, 0 deletions
diff --git a/src/mistune/plugins/__init__.py b/src/mistune/plugins/__init__.py
new file mode 100644
index 0000000..a79d727
--- /dev/null
+++ b/src/mistune/plugins/__init__.py
@@ -0,0 +1,38 @@
+from importlib import import_module
+
+_plugins = {
+ 'speedup': 'mistune.plugins.speedup.speedup',
+ 'strikethrough': 'mistune.plugins.formatting.strikethrough',
+ 'mark': 'mistune.plugins.formatting.mark',
+ 'insert': 'mistune.plugins.formatting.insert',
+ 'superscript': 'mistune.plugins.formatting.superscript',
+ 'subscript': 'mistune.plugins.formatting.subscript',
+ 'footnotes': 'mistune.plugins.footnotes.footnotes',
+ 'table': 'mistune.plugins.table.table',
+ 'url': 'mistune.plugins.url.url',
+ 'abbr': 'mistune.plugins.abbr.abbr',
+ 'def_list': 'mistune.plugins.def_list.def_list',
+ 'math': 'mistune.plugins.math.math',
+ 'ruby': 'mistune.plugins.ruby.ruby',
+ 'task_lists': 'mistune.plugins.task_lists.task_lists',
+ 'spoiler': 'mistune.plugins.spoiler.spoiler',
+}
+_cached_modules = {}
+
+
+def import_plugin(name):
+ if name in _cached_modules:
+ return _cached_modules[name]
+
+ if callable(name):
+ return name
+
+ if name in _plugins:
+ module_path, func_name = _plugins[name].rsplit(".", 1)
+ else:
+ module_path, func_name = name.rsplit(".", 1)
+
+ module = import_module(module_path)
+ plugin = getattr(module, func_name)
+ _cached_modules[name] = plugin
+ return plugin
diff --git a/src/mistune/plugins/abbr.py b/src/mistune/plugins/abbr.py
new file mode 100644
index 0000000..1b45790
--- /dev/null
+++ b/src/mistune/plugins/abbr.py
@@ -0,0 +1,103 @@
+import re
+import types
+from ..util import escape
+from ..helpers import PREVENT_BACKSLASH
+
+__all__ = ['abbr']
+
+# https://michelf.ca/projects/php-markdown/extra/#abbr
+REF_ABBR = (
+ r'^ {0,3}\*\[(?P<abbr_key>[^\]]+)'+ PREVENT_BACKSLASH + r'\]:'
+ r'(?P<abbr_text>(?:[ \t]*\n(?: {3,}|\t)[^\n]+)|(?:[^\n]*))$'
+)
+
+
+def parse_ref_abbr(block, m, state):
+ ref = state.env.get('ref_abbrs')
+ if not ref:
+ ref = {}
+ key = m.group('abbr_key')
+ text = m.group('abbr_text')
+ ref[key] = text.strip()
+ state.env['ref_abbrs'] = ref
+ # abbr definition can split paragraph
+ state.append_token({'type': 'blank_line'})
+ return m.end() + 1
+
+
+def process_text(inline, text, state):
+ ref = state.env.get('ref_abbrs')
+ if not ref:
+ return state.append_token({'type': 'text', 'raw': text})
+
+ if state.tokens:
+ last = state.tokens[-1]
+ if last['type'] == 'text':
+ state.tokens.pop()
+ text = last['raw'] + text
+
+ abbrs_re = state.env.get('abbrs_re')
+ if not abbrs_re:
+ abbrs_re = re.compile(r'|'.join(re.escape(k) for k in ref.keys()))
+ state.env['abbrs_re'] = abbrs_re
+
+ pos = 0
+ while pos < len(text):
+ m = abbrs_re.search(text, pos)
+ if not m:
+ break
+
+ end_pos = m.start()
+ if end_pos > pos:
+ hole = text[pos:end_pos]
+ state.append_token({'type': 'text', 'raw': hole})
+
+ label = m.group(0)
+ state.append_token({
+ 'type': 'abbr',
+ 'children': [{'type': 'text', 'raw': label}],
+ 'attrs': {'title': ref[label]}
+ })
+ pos = m.end()
+
+ if pos == 0:
+ # special case, just pure text
+ state.append_token({'type': 'text', 'raw': text})
+ elif pos < len(text):
+ state.append_token({'type': 'text', 'raw': text[pos:]})
+
+
+def render_abbr(renderer, text, title):
+ if not title:
+ return '<abbr>' + text + '</abbr>'
+ return '<abbr title="' + escape(title) + '">' + text + '</abbr>'
+
+
+def abbr(md):
+ """A mistune plugin to support abbreviations, spec defined at
+ https://michelf.ca/projects/php-markdown/extra/#abbr
+
+ Here is an example:
+
+ .. code-block:: text
+
+ The HTML specification
+ is maintained by the W3C.
+
+ *[HTML]: Hyper Text Markup Language
+ *[W3C]: World Wide Web Consortium
+
+ It will be converted into HTML:
+
+ .. code-block:: html
+
+ The <abbr title="Hyper Text Markup Language">HTML</abbr> specification
+ is maintained by the <abbr title="World Wide Web Consortium">W3C</abbr>.
+
+ :param md: Markdown instance
+ """
+ md.block.register('ref_abbr', REF_ABBR, parse_ref_abbr, before='paragraph')
+ # replace process_text
+ md.inline.process_text = types.MethodType(process_text, md.inline)
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('abbr', render_abbr)
diff --git a/src/mistune/plugins/def_list.py b/src/mistune/plugins/def_list.py
new file mode 100644
index 0000000..3675641
--- /dev/null
+++ b/src/mistune/plugins/def_list.py
@@ -0,0 +1,135 @@
+import re
+from ..util import strip_end
+
+__all__ = ['def_list']
+
+# https://michelf.ca/projects/php-markdown/extra/#def-list
+
+DEF_PATTERN = (
+ r'^(?P<def_list_head>(?:[^\n]+\n)+?)'
+ r'\n?(?:'
+ r'\:[ \t]+.*\n'
+ r'(?:[^\n]+\n)*' # lazy continue line
+ r'(?:(?:[ \t]*\n)*[ \t]+[^\n]+\n)*'
+ r'(?:[ \t]*\n)*'
+ r')+'
+)
+DEF_RE = re.compile(DEF_PATTERN, re.M)
+DD_START_RE = re.compile(r'^:[ \t]+', re.M)
+TRIM_RE = re.compile(r'^ {0,4}', re.M)
+HAS_BLANK_LINE_RE = re.compile(r'\n[ \t]*\n$')
+
+
+def parse_def_list(block, m, state):
+ pos = m.end()
+ children = list(_parse_def_item(block, m))
+
+ m = DEF_RE.match(state.src, pos)
+ while m:
+ children.extend(list(_parse_def_item(block, m)))
+ pos = m.end()
+ m = DEF_RE.match(state.src, pos)
+
+ state.append_token({
+ 'type': 'def_list',
+ 'children': children,
+ })
+ return pos
+
+
+def _parse_def_item(block, m):
+ head = m.group('def_list_head')
+ for line in head.splitlines():
+ yield {
+ 'type': 'def_list_head',
+ 'text': line,
+ }
+
+ src = m.group(0)
+ end = len(head)
+
+ m = DD_START_RE.search(src, end)
+ start = m.start()
+ prev_blank_line = src[end:start] == '\n'
+ while m:
+ m = DD_START_RE.search(src, start + 1)
+ if not m:
+ break
+
+ end = m.start()
+ text = src[start:end].replace(':', ' ', 1)
+ children = _process_text(block, text, prev_blank_line)
+ prev_blank_line = bool(HAS_BLANK_LINE_RE.search(text))
+ yield {
+ 'type': 'def_list_item',
+ 'children': children,
+ }
+ start = end
+
+ text = src[start:].replace(':', ' ', 1)
+ children = _process_text(block, text, prev_blank_line)
+ yield {
+ 'type': 'def_list_item',
+ 'children': children,
+ }
+
+
+def _process_text(block, text, loose):
+ text = TRIM_RE.sub('', text)
+ state = block.state_cls()
+ state.process(strip_end(text))
+ # use default list rules
+ block.parse(state, block.list_rules)
+ tokens = state.tokens
+ if not loose and len(tokens) == 1 and tokens[0]['type'] == 'paragraph':
+ tokens[0]['type'] = 'block_text'
+ return tokens
+
+
+def render_def_list(renderer, text):
+ return '<dl>\n' + text + '</dl>\n'
+
+
+def render_def_list_head(renderer, text):
+ return '<dt>' + text + '</dt>\n'
+
+
+def render_def_list_item(renderer, text):
+ return '<dd>' + text + '</dd>\n'
+
+
+def def_list(md):
+ """A mistune plugin to support def list, spec defined at
+ https://michelf.ca/projects/php-markdown/extra/#def-list
+
+ Here is an example:
+
+ .. code-block:: text
+
+ Apple
+ : Pomaceous fruit of plants of the genus Malus in
+ the family Rosaceae.
+
+ Orange
+ : The fruit of an evergreen tree of the genus Citrus.
+
+ It will be converted into HTML:
+
+ .. code-block:: html
+
+ <dl>
+ <dt>Apple</dt>
+ <dd>Pomaceous fruit of plants of the genus Malus in
+ the family Rosaceae.</dd>
+
+ <dt>Orange</dt>
+ <dd>The fruit of an evergreen tree of the genus Citrus.</dd>
+ </dl>
+
+ :param md: Markdown instance
+ """
+ md.block.register('def_list', DEF_PATTERN, parse_def_list, before='paragraph')
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('def_list', render_def_list)
+ md.renderer.register('def_list_head', render_def_list_head)
+ md.renderer.register('def_list_item', render_def_list_item)
diff --git a/src/mistune/plugins/footnotes.py b/src/mistune/plugins/footnotes.py
new file mode 100644
index 0000000..2e10704
--- /dev/null
+++ b/src/mistune/plugins/footnotes.py
@@ -0,0 +1,153 @@
+import re
+from ..core import BlockState
+from ..util import unikey
+from ..helpers import LINK_LABEL
+
+__all__ = ['footnotes']
+
+_PARAGRAPH_SPLIT = re.compile(r'\n{2,}')
+# https://michelf.ca/projects/php-markdown/extra/#footnotes
+REF_FOOTNOTE = (
+ r'^(?P<footnote_lead> {0,3})'
+ r'\[\^(?P<footnote_key>' + LINK_LABEL + r')]:[ \t]'
+ r'(?P<footnote_text>[^\n]*(?:\n+|$)'
+ r'(?:(?P=footnote_lead) {1,3}(?! )[^\n]*\n+)*'
+ r')'
+)
+
+INLINE_FOOTNOTE = r'\[\^(?P<footnote_key>' + LINK_LABEL + r')\]'
+
+
+def parse_inline_footnote(inline, m: re.Match, state):
+ key = unikey(m.group('footnote_key'))
+ ref = state.env.get('ref_footnotes')
+ if ref and key in ref:
+ notes = state.env.get('footnotes')
+ if not notes:
+ notes = []
+ if key not in notes:
+ notes.append(key)
+ state.env['footnotes'] = notes
+ state.append_token({
+ 'type': 'footnote_ref',
+ 'raw': key,
+ 'attrs': {'index': notes.index(key) + 1}
+ })
+ else:
+ state.append_token({'type': 'text', 'raw': m.group(0)})
+ return m.end()
+
+
+def parse_ref_footnote(block, m: re.Match, state: BlockState):
+ ref = state.env.get('ref_footnotes')
+ if not ref:
+ ref = {}
+
+ key = unikey(m.group('footnote_key'))
+ if key not in ref:
+ ref[key] = m.group('footnote_text')
+ state.env['ref_footnotes'] = ref
+ return m.end()
+
+
+def parse_footnote_item(block, key: str, index: int, state: BlockState):
+ ref = state.env.get('ref_footnotes')
+ text = ref[key]
+
+ lines = text.splitlines()
+ second_line = None
+ for second_line in lines[1:]:
+ if second_line:
+ break
+
+ if second_line:
+ spaces = len(second_line) - len(second_line.lstrip())
+ pattern = re.compile(r'^ {' + str(spaces) + r',}', flags=re.M)
+ text = pattern.sub('', text).strip()
+ items = _PARAGRAPH_SPLIT.split(text)
+ children = [{'type': 'paragraph', 'text': s} for s in items]
+ else:
+ text = text.strip()
+ children = [{'type': 'paragraph', 'text': text}]
+ return {
+ 'type': 'footnote_item',
+ 'children': children,
+ 'attrs': {'key': key, 'index': index}
+ }
+
+
+def md_footnotes_hook(md, result: str, state: BlockState):
+ notes = state.env.get('footnotes')
+ if not notes:
+ return result
+
+ children = [
+ parse_footnote_item(md.block, k, i + 1, state)
+ for i, k in enumerate(notes)
+ ]
+ state = BlockState()
+ state.tokens = [{'type': 'footnotes', 'children': children}]
+ output = md.render_state(state)
+ return result + output
+
+
+def render_footnote_ref(renderer, key: str, index: int):
+ i = str(index)
+ html = '<sup class="footnote-ref" id="fnref-' + i + '">'
+ return html + '<a href="#fn-' + i + '">' + i + '</a></sup>'
+
+
+def render_footnotes(renderer, text: str):
+ return '<section class="footnotes">\n<ol>\n' + text + '</ol>\n</section>\n'
+
+
+def render_footnote_item(renderer, text: str, key: str, index: int):
+ i = str(index)
+ back = '<a href="#fnref-' + i + '" class="footnote">&#8617;</a>'
+ text = text.rstrip()[:-4] + back + '</p>'
+ return '<li id="fn-' + i + '">' + text + '</li>\n'
+
+
+def footnotes(md):
+ """A mistune plugin to support footnotes, spec defined at
+ https://michelf.ca/projects/php-markdown/extra/#footnotes
+
+ Here is an example:
+
+ .. code-block:: text
+
+ That's some text with a footnote.[^1]
+
+ [^1]: And that's the footnote.
+
+ It will be converted into HTML:
+
+ .. code-block:: html
+
+ <p>That's some text with a footnote.<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup></p>
+ <section class="footnotes">
+ <ol>
+ <li id="fn-1"><p>And that's the footnote.<a href="#fnref-1" class="footnote">&#8617;</a></p></li>
+ </ol>
+ </section>
+
+ :param md: Markdown instance
+ """
+ md.inline.register(
+ 'footnote',
+ INLINE_FOOTNOTE,
+ parse_inline_footnote,
+ before='link',
+ )
+ md.block.register(
+ 'ref_footnote',
+ REF_FOOTNOTE,
+ parse_ref_footnote,
+ before='ref_link',
+ )
+ md.after_render_hooks.append(md_footnotes_hook)
+
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('footnote_ref', render_footnote_ref)
+ md.renderer.register('footnote_item', render_footnote_item)
+ md.renderer.register('footnotes', render_footnotes)
diff --git a/src/mistune/plugins/formatting.py b/src/mistune/plugins/formatting.py
new file mode 100644
index 0000000..57e5def
--- /dev/null
+++ b/src/mistune/plugins/formatting.py
@@ -0,0 +1,173 @@
+import re
+from ..helpers import PREVENT_BACKSLASH
+
+__all__ = ["strikethrough", "mark", "insert", "superscript", "subscript"]
+
+_STRIKE_END = re.compile(r'(?:' + PREVENT_BACKSLASH + r'\\~|[^\s~])~~(?!~)')
+_MARK_END = re.compile(r'(?:' + PREVENT_BACKSLASH + r'\\=|[^\s=])==(?!=)')
+_INSERT_END = re.compile(r'(?:' + PREVENT_BACKSLASH + r'\\\^|[^\s^])\^\^(?!\^)')
+
+SUPERSCRIPT_PATTERN = r'\^(?:' + PREVENT_BACKSLASH + r'\\\^|\S|\\ )+?\^'
+SUBSCRIPT_PATTERN = r'~(?:' + PREVENT_BACKSLASH + r'\\~|\S|\\ )+?~'
+
+
+def parse_strikethrough(inline, m, state):
+ return _parse_to_end(inline, m, state, 'strikethrough', _STRIKE_END)
+
+
+def render_strikethrough(renderer, text):
+ return '<del>' + text + '</del>'
+
+
+def parse_mark(inline, m, state):
+ return _parse_to_end(inline, m, state, 'mark', _MARK_END)
+
+
+def render_mark(renderer, text):
+ return '<mark>' + text + '</mark>'
+
+
+def parse_insert(inline, m, state):
+ return _parse_to_end(inline, m, state, 'insert', _INSERT_END)
+
+
+def render_insert(renderer, text):
+ return '<ins>' + text + '</ins>'
+
+
+def parse_superscript(inline, m, state):
+ return _parse_script(inline, m, state, 'superscript')
+
+
+def render_superscript(renderer, text):
+ return '<sup>' + text + '</sup>'
+
+
+def parse_subscript(inline, m, state):
+ return _parse_script(inline, m, state, 'subscript')
+
+
+def render_subscript(renderer, text):
+ return '<sub>' + text + '</sub>'
+
+
+def _parse_to_end(inline, m, state, tok_type, end_pattern):
+ pos = m.end()
+ m1 = end_pattern.search(state.src, pos)
+ if not m1:
+ return
+ end_pos = m1.end()
+ text = state.src[pos:end_pos-2]
+ new_state = state.copy()
+ new_state.src = text
+ children = inline.render(new_state)
+ state.append_token({'type': tok_type, 'children': children})
+ return end_pos
+
+
+def _parse_script(inline, m, state, tok_type):
+ text = m.group(0)
+ new_state = state.copy()
+ new_state.src = text[1:-1].replace('\\ ', ' ')
+ children = inline.render(new_state)
+ state.append_token({
+ 'type': tok_type,
+ 'children': children
+ })
+ return m.end()
+
+
+def strikethrough(md):
+ """A mistune plugin to support strikethrough. Spec defined by
+ GitHub flavored Markdown and commonly used by many parsers:
+
+ .. code-block:: text
+
+ ~~This was mistaken text~~
+
+ It will be converted into HTML:
+
+ .. code-block:: html
+
+ <del>This was mistaken text</del>
+
+ :param md: Markdown instance
+ """
+ md.inline.register(
+ 'strikethrough',
+ r'~~(?=[^\s~])',
+ parse_strikethrough,
+ before='link',
+ )
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('strikethrough', render_strikethrough)
+
+
+def mark(md):
+ """A mistune plugin to add ``<mark>`` tag. Spec defined at
+ https://facelessuser.github.io/pymdown-extensions/extensions/mark/:
+
+ .. code-block:: text
+
+ ==mark me== ==mark \\=\\= equal==
+
+ :param md: Markdown instance
+ """
+ md.inline.register(
+ 'mark',
+ r'==(?=[^\s=])',
+ parse_mark,
+ before='link',
+ )
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('mark', render_mark)
+
+
+def insert(md):
+ """A mistune plugin to add ``<ins>`` tag. Spec defined at
+ https://facelessuser.github.io/pymdown-extensions/extensions/caret/#insert:
+
+ .. code-block:: text
+
+ ^^insert me^^
+
+ :param md: Markdown instance
+ """
+ md.inline.register(
+ 'insert',
+ r'\^\^(?=[^\s\^])',
+ parse_insert,
+ before='link',
+ )
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('insert', render_insert)
+
+
+def superscript(md):
+ """A mistune plugin to add ``<sup>`` tag. Spec defined at
+ https://pandoc.org/MANUAL.html#superscripts-and-subscripts:
+
+ .. code-block:: text
+
+ 2^10^ is 1024.
+
+ :param md: Markdown instance
+ """
+ md.inline.register('superscript', SUPERSCRIPT_PATTERN, parse_superscript, before='linebreak')
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('superscript', render_superscript)
+
+
+def subscript(md):
+ """A mistune plugin to add ``<sub>`` tag. Spec defined at
+ https://pandoc.org/MANUAL.html#superscripts-and-subscripts:
+
+ .. code-block:: text
+
+ H~2~O is a liquid.
+
+ :param md: Markdown instance
+ """
+ md.inline.register('subscript', SUBSCRIPT_PATTERN, parse_subscript, before='linebreak')
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('subscript', render_subscript)
diff --git a/src/mistune/plugins/math.py b/src/mistune/plugins/math.py
new file mode 100644
index 0000000..805105e
--- /dev/null
+++ b/src/mistune/plugins/math.py
@@ -0,0 +1,57 @@
+__all__ = ['math', 'math_in_quote', 'math_in_list']
+
+BLOCK_MATH_PATTERN = r'^ {0,3}\$\$[ \t]*\n(?P<math_text>[\s\S]+?)\n\$\$[ \t]*$'
+INLINE_MATH_PATTERN = r'\$(?!\s)(?P<math_text>.+?)(?!\s)\$'
+
+
+def parse_block_math(block, m, state):
+ text = m.group('math_text')
+ state.append_token({'type': 'block_math', 'raw': text})
+ return m.end() + 1
+
+
+def parse_inline_math(inline, m, state):
+ text = m.group('math_text')
+ state.append_token({'type': 'inline_math', 'raw': text})
+ return m.end()
+
+
+def render_block_math(renderer, text):
+ return '<div class="math">$$\n' + text + '\n$$</div>\n'
+
+
+def render_inline_math(renderer, text):
+ return r'<span class="math">\(' + text + r'\)</span>'
+
+
+def math(md):
+ """A mistune plugin to support math. The syntax is used
+ by many markdown extensions:
+
+ .. code-block:: text
+
+ Block math is surrounded by $$:
+
+ $$
+ f(a)=f(b)
+ $$
+
+ Inline math is surrounded by `$`, such as $f(a)=f(b)$
+
+ :param md: Markdown instance
+ """
+ md.block.register('block_math', BLOCK_MATH_PATTERN, parse_block_math, before='list')
+ md.inline.register('inline_math', INLINE_MATH_PATTERN, parse_inline_math, before='link')
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('block_math', render_block_math)
+ md.renderer.register('inline_math', render_inline_math)
+
+
+def math_in_quote(md):
+ """Enable block math plugin in block quote."""
+ md.block.insert_rule(md.block.block_quote_rules, 'block_math', before='list')
+
+
+def math_in_list(md):
+ """Enable block math plugin in list."""
+ md.block.insert_rule(md.block.list_rules, 'block_math', before='list')
diff --git a/src/mistune/plugins/ruby.py b/src/mistune/plugins/ruby.py
new file mode 100644
index 0000000..eabc037
--- /dev/null
+++ b/src/mistune/plugins/ruby.py
@@ -0,0 +1,100 @@
+import re
+from ..util import unikey
+from ..helpers import parse_link, parse_link_label
+
+
+RUBY_PATTERN = r'\[(?:\w+\(\w+\))+\]'
+_ruby_re = re.compile(RUBY_PATTERN)
+
+
+def parse_ruby(inline, m, state):
+ text = m.group(0)[1:-2]
+ items = text.split(')')
+ tokens = []
+ for item in items:
+ rb, rt = item.split('(')
+ tokens.append({
+ 'type': 'ruby',
+ 'raw': rb,
+ 'attrs': {'rt': rt}
+ })
+
+ end_pos = m.end()
+
+ next_match = _ruby_re.match(state.src, end_pos)
+ if next_match:
+ for tok in tokens:
+ state.append_token(tok)
+ return parse_ruby(inline, next_match, state)
+
+ # repeat link logic
+ if end_pos < len(state.src):
+ link_pos = _parse_ruby_link(inline, state, end_pos, tokens)
+ if link_pos:
+ return link_pos
+
+ for tok in tokens:
+ state.append_token(tok)
+ return end_pos
+
+
+def _parse_ruby_link(inline, state, pos, tokens):
+ c = state.src[pos]
+ if c == '(':
+ # standard link [text](<url> "title")
+ attrs, link_pos = parse_link(state.src, pos + 1)
+ if link_pos:
+ state.append_token({
+ 'type': 'link',
+ 'children': tokens,
+ 'attrs': attrs,
+ })
+ return link_pos
+
+ elif c == '[':
+ # standard ref link [text][label]
+ label, link_pos = parse_link_label(state.src, pos + 1)
+ if label and link_pos:
+ ref_links = state.env['ref_links']
+ key = unikey(label)
+ env = ref_links.get(key)
+ if env:
+ attrs = {'url': env['url'], 'title': env.get('title')}
+ state.append_token({
+ 'type': 'link',
+ 'children': tokens,
+ 'attrs': attrs,
+ })
+ else:
+ for tok in tokens:
+ state.append_token(tok)
+ state.append_token({
+ 'type': 'text',
+ 'raw': '[' + label + ']',
+ })
+ return link_pos
+
+
+def render_ruby(renderer, text, rt):
+ return '<ruby><rb>' + text + '</rb><rt>' + rt + '</rt></ruby>'
+
+
+def ruby(md):
+ """A mistune plugin to support ``<ruby>`` tag. The syntax is defined
+ at https://lepture.com/en/2022/markdown-ruby-markup:
+
+ .. code-block:: text
+
+ [漢字(ㄏㄢˋㄗˋ)]
+ [漢(ㄏㄢˋ)字(ㄗˋ)]
+
+ [漢字(ㄏㄢˋㄗˋ)][link]
+ [漢字(ㄏㄢˋㄗˋ)](/url "title")
+
+ [link]: /url "title"
+
+ :param md: Markdown instance
+ """
+ md.inline.register('ruby', RUBY_PATTERN, parse_ruby, before='link')
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('ruby', render_ruby)
diff --git a/src/mistune/plugins/speedup.py b/src/mistune/plugins/speedup.py
new file mode 100644
index 0000000..784022c
--- /dev/null
+++ b/src/mistune/plugins/speedup.py
@@ -0,0 +1,44 @@
+import re
+import string
+
+# because mismatch is too slow, add parsers for paragraph and text
+
+HARD_LINEBREAK_RE = re.compile(r' *\n\s*')
+PARAGRAPH = (
+ # start with none punctuation, not number, not whitespace
+ r'(?:^[^\s\d' + re.escape(string.punctuation) + r'][^\n]*\n)+'
+)
+
+__all__ = ['speedup']
+
+
+
+def parse_text(inline, m, state):
+ text = m.group(0)
+ text = HARD_LINEBREAK_RE.sub('\n', text)
+ inline.process_text(text, state)
+ return m.end()
+
+
+def parse_paragraph(block, m, state):
+ text = m.group(0)
+ state.add_paragraph(text)
+ return m.end()
+
+
+def speedup(md):
+ """Increase the speed of parsing paragraph and inline text."""
+ md.block.register('paragraph', PARAGRAPH, parse_paragraph)
+
+ punc = r'\\><!\[_*`~\^\$='
+ text_pattern = r'[\s\S]+?(?=[' + punc + r']|'
+ if 'url_link' in md.inline.rules:
+ text_pattern += 'https?:|'
+
+ if md.inline.hard_wrap:
+ text_pattern += r' *\n|'
+ else:
+ text_pattern += r' {2,}\n|'
+
+ text_pattern += r'$)'
+ md.inline.register('text', text_pattern, parse_text)
diff --git a/src/mistune/plugins/spoiler.py b/src/mistune/plugins/spoiler.py
new file mode 100644
index 0000000..2931d2b
--- /dev/null
+++ b/src/mistune/plugins/spoiler.py
@@ -0,0 +1,80 @@
+import re
+
+__all__ = ['spoiler']
+
+_BLOCK_SPOILER_START = re.compile(r'^ {0,3}! ?', re.M)
+_BLOCK_SPOILER_MATCH = re.compile(r'^( {0,3}![^\n]*\n)+$')
+
+INLINE_SPOILER_PATTERN = r'>!\s*(?P<spoiler_text>.+?)\s*!<'
+
+
+def parse_block_spoiler(block, m, state):
+ text, end_pos = block.extract_block_quote(m, state)
+ if not text.endswith('\n'):
+ # ensure it endswith \n to make sure
+ # _BLOCK_SPOILER_MATCH.match works
+ text += '\n'
+
+ depth = state.depth()
+ if not depth and _BLOCK_SPOILER_MATCH.match(text):
+ text = _BLOCK_SPOILER_START.sub('', text)
+ tok_type = 'block_spoiler'
+ else:
+ tok_type = 'block_quote'
+
+ # scan children state
+ child = state.child_state(text)
+ if state.depth() >= block.max_nested_level - 1:
+ rules = list(block.block_quote_rules)
+ rules.remove('block_quote')
+ else:
+ rules = block.block_quote_rules
+
+ block.parse(child, rules)
+ token = {'type': tok_type, 'children': child.tokens}
+ if end_pos:
+ state.prepend_token(token)
+ return end_pos
+ state.append_token(token)
+ return state.cursor
+
+
+def parse_inline_spoiler(inline, m, state):
+ text = m.group('spoiler_text')
+ new_state = state.copy()
+ new_state.src = text
+ children = inline.render(new_state)
+ state.append_token({'type': 'inline_spoiler', 'children': children})
+ return m.end()
+
+
+def render_block_spoiler(renderer, text):
+ return '<div class="spoiler">\n' + text + '</div>\n'
+
+
+def render_inline_spoiler(renderer, text):
+ return '<span class="spoiler">' + text + '</span>'
+
+
+def spoiler(md):
+ """A mistune plugin to support block and inline spoiler. The
+ syntax is inspired by stackexchange:
+
+ .. code-block:: text
+
+ Block level spoiler looks like block quote, but with `>!`:
+
+ >! this is spoiler
+ >!
+ >! the content will be hidden
+
+ Inline spoiler is surrounded by `>!` and `!<`, such as >! hide me !<.
+
+ :param md: Markdown instance
+ """
+ # reset block quote parser with block spoiler parser
+ md.block.register('block_quote', None, parse_block_spoiler)
+ md.inline.register('inline_spoiler', INLINE_SPOILER_PATTERN, parse_inline_spoiler)
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('block_spoiler', render_block_spoiler)
+ md.renderer.register('inline_spoiler', render_inline_spoiler)
diff --git a/src/mistune/plugins/table.py b/src/mistune/plugins/table.py
new file mode 100644
index 0000000..d3bc4c2
--- /dev/null
+++ b/src/mistune/plugins/table.py
@@ -0,0 +1,179 @@
+import re
+from ..helpers import PREVENT_BACKSLASH
+
+# https://michelf.ca/projects/php-markdown/extra/#table
+
+__all__ = ['table', 'table_in_quote', 'table_in_list']
+
+
+TABLE_PATTERN = (
+ r'^ {0,3}\|(?P<table_head>.+)\|[ \t]*\n'
+ r' {0,3}\|(?P<table_align> *[-:]+[-| :]*)\|[ \t]*\n'
+ r'(?P<table_body>(?: {0,3}\|.*\|[ \t]*(?:\n|$))*)\n*'
+)
+NP_TABLE_PATTERN = (
+ r'^ {0,3}(?P<nptable_head>\S.*\|.*)\n'
+ r' {0,3}(?P<nptable_align>[-:]+ *\|[-| :]*)\n'
+ r'(?P<nptable_body>(?:.*\|.*(?:\n|$))*)\n*'
+)
+
+TABLE_CELL = re.compile(r'^ {0,3}\|(.+)\|[ \t]*$')
+CELL_SPLIT = re.compile(r' *' + PREVENT_BACKSLASH + r'\| *')
+ALIGN_CENTER = re.compile(r'^ *:-+: *$')
+ALIGN_LEFT = re.compile(r'^ *:-+ *$')
+ALIGN_RIGHT = re.compile(r'^ *-+: *$')
+
+
+def parse_table(block, m, state):
+ pos = m.end()
+ header = m.group('table_head')
+ align = m.group('table_align')
+ thead, aligns = _process_thead(header, align)
+ if not thead:
+ return
+
+ rows = []
+ body = m.group('table_body')
+ for text in body.splitlines():
+ m = TABLE_CELL.match(text)
+ if not m: # pragma: no cover
+ return
+ row = _process_row(m.group(1), aligns)
+ if not row:
+ return
+ rows.append(row)
+
+ children = [thead, {'type': 'table_body', 'children': rows}]
+ state.append_token({'type': 'table', 'children': children})
+ return pos
+
+
+def parse_nptable(block, m, state):
+ header = m.group('nptable_head')
+ align = m.group('nptable_align')
+ thead, aligns = _process_thead(header, align)
+ if not thead:
+ return
+
+ rows = []
+ body = m.group('nptable_body')
+ for text in body.splitlines():
+ row = _process_row(text, aligns)
+ if not row:
+ return
+ rows.append(row)
+
+ children = [thead, {'type': 'table_body', 'children': rows}]
+ state.append_token({'type': 'table', 'children': children})
+ return m.end()
+
+
+def _process_thead(header, align):
+ headers = CELL_SPLIT.split(header)
+ aligns = CELL_SPLIT.split(align)
+ if len(headers) != len(aligns):
+ return None, None
+
+ for i, v in enumerate(aligns):
+ if ALIGN_CENTER.match(v):
+ aligns[i] = 'center'
+ elif ALIGN_LEFT.match(v):
+ aligns[i] = 'left'
+ elif ALIGN_RIGHT.match(v):
+ aligns[i] = 'right'
+ else:
+ aligns[i] = None
+
+ children = [
+ {
+ 'type': 'table_cell',
+ 'text': text.strip(),
+ 'attrs': {'align': aligns[i], 'head': True}
+ }
+ for i, text in enumerate(headers)
+ ]
+ thead = {'type': 'table_head', 'children': children}
+ return thead, aligns
+
+
+def _process_row(text, aligns):
+ cells = CELL_SPLIT.split(text)
+ if len(cells) != len(aligns):
+ return None
+
+ children = [
+ {
+ 'type': 'table_cell',
+ 'text': text.strip(),
+ 'attrs': {'align': aligns[i], 'head': False}
+ }
+ for i, text in enumerate(cells)
+ ]
+ return {'type': 'table_row', 'children': children}
+
+
+def render_table(renderer, text):
+ return '<table>\n' + text + '</table>\n'
+
+
+def render_table_head(renderer, text):
+ return '<thead>\n<tr>\n' + text + '</tr>\n</thead>\n'
+
+
+def render_table_body(renderer, text):
+ return '<tbody>\n' + text + '</tbody>\n'
+
+
+def render_table_row(renderer, text):
+ return '<tr>\n' + text + '</tr>\n'
+
+
+def render_table_cell(renderer, text, align=None, head=False):
+ if head:
+ tag = 'th'
+ else:
+ tag = 'td'
+
+ html = ' <' + tag
+ if align:
+ html += ' style="text-align:' + align + '"'
+
+ return html + '>' + text + '</' + tag + '>\n'
+
+
+def table(md):
+ """A mistune plugin to support table, spec defined at
+ https://michelf.ca/projects/php-markdown/extra/#table
+
+ Here is an example:
+
+ .. code-block:: text
+
+ First Header | Second Header
+ ------------- | -------------
+ Content Cell | Content Cell
+ Content Cell | Content Cell
+
+ :param md: Markdown instance
+ """
+ md.block.register('table', TABLE_PATTERN, parse_table, before='paragraph')
+ md.block.register('nptable', NP_TABLE_PATTERN, parse_nptable, before='paragraph')
+
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('table', render_table)
+ md.renderer.register('table_head', render_table_head)
+ md.renderer.register('table_body', render_table_body)
+ md.renderer.register('table_row', render_table_row)
+ md.renderer.register('table_cell', render_table_cell)
+
+
+def table_in_quote(md):
+ """Enable table plugin in block quotes."""
+ md.block.insert_rule(md.block.block_quote_rules, 'table', before='paragraph')
+ md.block.insert_rule(md.block.block_quote_rules, 'nptable', before='paragraph')
+
+
+def table_in_list(md):
+ """Enable table plugin in list."""
+ md.block.insert_rule(md.block.list_rules, 'table', before='paragraph')
+ md.block.insert_rule(md.block.list_rules, 'nptable', before='paragraph')
diff --git a/src/mistune/plugins/task_lists.py b/src/mistune/plugins/task_lists.py
new file mode 100644
index 0000000..8571c32
--- /dev/null
+++ b/src/mistune/plugins/task_lists.py
@@ -0,0 +1,67 @@
+import re
+
+__all__ = ['task_lists']
+
+
+TASK_LIST_ITEM = re.compile(r'^(\[[ xX]\])\s+')
+
+
+def task_lists_hook(md, state):
+ return _rewrite_all_list_items(state.tokens)
+
+
+def render_task_list_item(renderer, text, checked=False):
+ checkbox = (
+ '<input class="task-list-item-checkbox" '
+ 'type="checkbox" disabled'
+ )
+ if checked:
+ checkbox += ' checked/>'
+ else:
+ checkbox += '/>'
+
+ if text.startswith('<p>'):
+ text = text.replace('<p>', '<p>' + checkbox, 1)
+ else:
+ text = checkbox + text
+
+ return '<li class="task-list-item">' + text + '</li>\n'
+
+
+def task_lists(md):
+ """A mistune plugin to support task lists. Spec defined by
+ GitHub flavored Markdown and commonly used by many parsers:
+
+ .. code-block:: text
+
+ - [ ] unchecked task
+ - [x] checked task
+
+ :param md: Markdown instance
+ """
+ md.before_render_hooks.append(task_lists_hook)
+ if md.renderer and md.renderer.NAME == 'html':
+ md.renderer.register('task_list_item', render_task_list_item)
+
+
+def _rewrite_all_list_items(tokens):
+ for tok in tokens:
+ if tok['type'] == 'list_item':
+ _rewrite_list_item(tok)
+ if 'children' in tok:
+ _rewrite_all_list_items(tok['children'])
+ return tokens
+
+
+def _rewrite_list_item(tok):
+ children = tok['children']
+ if children:
+ first_child = children[0]
+ text = first_child.get('text', '')
+ m = TASK_LIST_ITEM.match(text)
+ if m:
+ mark = m.group(1)
+ first_child['text'] = text[m.end():]
+
+ tok['type'] = 'task_list_item'
+ tok['attrs'] = {'checked': mark != '[ ]'}
diff --git a/src/mistune/plugins/url.py b/src/mistune/plugins/url.py
new file mode 100644
index 0000000..d6f2251
--- /dev/null
+++ b/src/mistune/plugins/url.py
@@ -0,0 +1,23 @@
+from ..util import escape_url
+
+__all__ = ['url']
+
+URL_LINK_PATTERN = r'''https?:\/\/[^\s<]+[^<.,:;"')\]\s]'''
+
+
+def parse_url_link(inline, m, state):
+ text = m.group(0)
+ pos = m.end()
+ if state.in_link:
+ inline.process_text(text, state)
+ return pos
+ state.append_token({
+ 'type': 'link',
+ 'children': [{'type': 'text', 'raw': text}],
+ 'attrs': {'url': escape_url(text)},
+ })
+ return pos
+
+
+def url(md):
+ md.inline.register('url_link', URL_LINK_PATTERN, parse_url_link)