diff options
Diffstat (limited to 'src/mistune/plugins')
-rw-r--r-- | src/mistune/plugins/__init__.py | 38 | ||||
-rw-r--r-- | src/mistune/plugins/abbr.py | 103 | ||||
-rw-r--r-- | src/mistune/plugins/def_list.py | 135 | ||||
-rw-r--r-- | src/mistune/plugins/footnotes.py | 153 | ||||
-rw-r--r-- | src/mistune/plugins/formatting.py | 173 | ||||
-rw-r--r-- | src/mistune/plugins/math.py | 57 | ||||
-rw-r--r-- | src/mistune/plugins/ruby.py | 100 | ||||
-rw-r--r-- | src/mistune/plugins/speedup.py | 44 | ||||
-rw-r--r-- | src/mistune/plugins/spoiler.py | 80 | ||||
-rw-r--r-- | src/mistune/plugins/table.py | 179 | ||||
-rw-r--r-- | src/mistune/plugins/task_lists.py | 67 | ||||
-rw-r--r-- | src/mistune/plugins/url.py | 23 |
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">↩</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">↩</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) |