[Populate] Included mistune

- The dependency is too narrow mistune>=2.0.2, <=2.0.5
- Debian Sid uses 3.x
- The API changes too often and the author doesn't provide backwards
  compatibility
This commit is contained in:
Salvador E. Tropea 2024-03-20 06:46:52 -03:00
parent 1cbbed1414
commit b7add3644d
24 changed files with 2193 additions and 89 deletions

View File

@ -71,19 +71,15 @@
- Mandatory for `kikit_present`
`mistune <https://pypi.org/project/mistune/>`__ :index:`: <pair: dependency; mistune>` |image29| |image30|
- Mandatory for `populate`
`QRCodeGen <https://pypi.org/project/QRCodeGen/>`__ :index:`: <pair: dependency; QRCodeGen>` |image31| |image32| |image33| |Auto-download|
`QRCodeGen <https://pypi.org/project/QRCodeGen/>`__ :index:`: <pair: dependency; QRCodeGen>` |image29| |image30| |image31| |Auto-download|
- Mandatory for `qr_lib`
`Colorama <https://pypi.org/project/Colorama/>`__ :index:`: <pair: dependency; Colorama>` |image34| |image35| |image36|
`Colorama <https://pypi.org/project/Colorama/>`__ :index:`: <pair: dependency; Colorama>` |image32| |image33| |image34|
- Optional to get color messages in a portable way for general use
`Git <https://git-scm.com/>`__ :index:`: <pair: dependency; Git>` |image37| |image38| |Auto-download|
`Git <https://git-scm.com/>`__ :index:`: <pair: dependency; Git>` |image35| |image36| |Auto-download|
- Optional to:
@ -94,7 +90,7 @@
- Find commit hash and/or date for `sch_replace`
- Find commit hash and/or date for `set_text_variables`
`ImageMagick <https://imagemagick.org/>`__ :index:`: <pair: dependency; ImageMagick>` |image39| |image40| |Auto-download|
`ImageMagick <https://imagemagick.org/>`__ :index:`: <pair: dependency; ImageMagick>` |image37| |image38| |Auto-download|
- Optional to:
@ -104,7 +100,7 @@
- Create JPG and BMP images for `pcbdraw`
- Automatically crop images for `render_3d`
`RSVG tools <https://gitlab.gnome.org/GNOME/librsvg>`__ :index:`: <pair: dependency; RSVG tools>` |image41| |image42| |Auto-download|
`RSVG tools <https://gitlab.gnome.org/GNOME/librsvg>`__ :index:`: <pair: dependency; RSVG tools>` |image39| |image40| |Auto-download|
- Optional to:
@ -113,7 +109,7 @@
- Create PDF, PNG, PS and EPS formats for `pcb_print`
- Create PNG, JPG and BMP images for `pcbdraw`
`Bash <https://www.gnu.org/software/bash/>`__ :index:`: <pair: dependency; Bash>` |image43| |image44|
`Bash <https://www.gnu.org/software/bash/>`__ :index:`: <pair: dependency; Bash>` |image41| |image42|
- Optional to:
@ -121,27 +117,27 @@
- Run external commands to create replacement text for `sch_replace`
- Run external commands to create replacement text for `set_text_variables`
`Ghostscript <https://www.ghostscript.com/>`__ :index:`: <pair: dependency; Ghostscript>` |image45| |image46| |Auto-download|
`Ghostscript <https://www.ghostscript.com/>`__ :index:`: <pair: dependency; Ghostscript>` |image43| |image44| |Auto-download|
- Optional to:
- Create outputs preview for `navigate_results`
- Create PNG, PS and EPS formats for `pcb_print`
`numpy <https://pypi.org/project/numpy/>`__ :index:`: <pair: dependency; numpy>` |image47| |image48| |Auto-download|
`numpy <https://pypi.org/project/numpy/>`__ :index:`: <pair: dependency; numpy>` |image45| |image46| |Auto-download|
- Optional to automatically adjust SVG margin for `pcbdraw`
`Pandoc <https://pandoc.org/>`__ :index:`: <pair: dependency; Pandoc>` |image49| |image50|
`Pandoc <https://pandoc.org/>`__ :index:`: <pair: dependency; Pandoc>` |image47| |image48|
- Optional to create PDF/ODF/DOCX files for `report`
- Note: In CI/CD environments: the `kicad_auto_test` docker image contains it.
`RAR <https://www.rarlab.com/>`__ :index:`: <pair: dependency; RAR>` |image51| |image52| |Auto-download|
`RAR <https://www.rarlab.com/>`__ :index:`: <pair: dependency; RAR>` |image49| |image50| |Auto-download|
- Optional to compress in RAR format for `compress`
`XLSXWriter <https://pypi.org/project/XLSXWriter/>`__ :index:`: <pair: dependency; XLSXWriter>` v1.1.2 |image53| |image54| |image55| |Auto-download|
`XLSXWriter <https://pypi.org/project/XLSXWriter/>`__ :index:`: <pair: dependency; XLSXWriter>` v1.1.2 |image51| |image52| |image53| |Auto-download|
- Optional to create XLSX files for `bom`
@ -204,57 +200,53 @@
.. |image28| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-markdown2
.. |image29| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
:target: https://pypi.org/project/mistune/
.. |image30| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-mistune
.. |image31| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
:target: https://pypi.org/project/QRCodeGen/
.. |image32| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
.. |image30| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
:target: https://pypi.org/project/QRCodeGen/
.. |image33| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image31| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-qrcodegen
.. |image34| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
.. |image32| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
:target: https://pypi.org/project/Colorama/
.. |image35| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
.. |image33| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
:target: https://pypi.org/project/Colorama/
.. |image36| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image34| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-colorama
.. |image37| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image35| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://git-scm.com/
.. |image38| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image36| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/git
.. |image39| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image37| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://imagemagick.org/
.. |image40| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image38| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/imagemagick
.. |image41| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image39| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://gitlab.gnome.org/GNOME/librsvg
.. |image42| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image40| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/librsvg2-bin
.. |image43| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image41| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://www.gnu.org/software/bash/
.. |image44| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image42| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/bash
.. |image45| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image43| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://www.ghostscript.com/
.. |image46| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image44| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/ghostscript
.. |image47| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
.. |image45| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
:target: https://pypi.org/project/numpy/
.. |image48| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image46| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-numpy
.. |image49| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image47| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://pandoc.org/
.. |image50| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image48| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/pandoc
.. |image51| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
.. |image49| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png
:target: https://www.rarlab.com/
.. |image52| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image50| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/rar
.. |image53| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
.. |image51| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png
:target: https://pypi.org/project/XLSXWriter/
.. |image54| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
.. |image52| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png
:target: https://pypi.org/project/XLSXWriter/
.. |image55| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
.. |image53| image:: https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png
:target: https://packages.debian.org/stable/python3-xlsxwriter

View File

@ -16,7 +16,7 @@
# The following try-catch is used to support mistune 0.8.4 and 2.x
try:
from mistune.renderers import BaseRenderer # type: ignore
from .mistune.renderers import BaseRenderer # type: ignore
except ModuleNotFoundError:
from mistune import Renderer # type: ignore
BaseRenderer = Renderer

View File

@ -0,0 +1,63 @@
from .markdown import Markdown
from .block_parser import BlockParser
from .inline_parser import InlineParser
from .renderers import AstRenderer, HTMLRenderer
from .plugins import PLUGINS
from .util import escape, escape_url, escape_html, unikey
def create_markdown(escape=True, hard_wrap=False, renderer=None, plugins=None):
"""Create a Markdown instance based on the given condition.
:param escape: Boolean. If using html renderer, escape html.
:param hard_wrap: Boolean. Break every new line into ``<br>``.
:param renderer: renderer instance or string of ``html`` and ``ast``.
:param plugins: List of plugins, string or callable.
This method is used when you want to re-use a Markdown instance::
markdown = create_markdown(
escape=False,
renderer='html',
plugins=['url', 'strikethrough', 'footnotes', 'table'],
)
# re-use markdown function
markdown('.... your text ...')
"""
if renderer is None or renderer == 'html':
renderer = HTMLRenderer(escape=escape)
elif renderer == 'ast':
renderer = AstRenderer()
if plugins:
_plugins = []
for p in plugins:
if isinstance(p, str):
_plugins.append(PLUGINS[p])
else:
_plugins.append(p)
plugins = _plugins
return Markdown(renderer, inline=InlineParser(renderer, hard_wrap=hard_wrap), plugins=plugins)
html = create_markdown(
escape=False,
renderer='html',
plugins=['strikethrough', 'footnotes', 'table'],
)
def markdown(text, escape=True, renderer=None, plugins=None):
md = create_markdown(escape=escape, renderer=renderer, plugins=plugins)
return md(text)
__all__ = [
'Markdown', 'AstRenderer', 'HTMLRenderer',
'BlockParser', 'InlineParser',
'escape', 'escape_url', 'escape_html', 'unikey',
'html', 'create_markdown', 'markdown',
]
__version__ = '2.0.4'

View File

@ -0,0 +1,366 @@
import re
from .scanner import ScannerParser, Matcher
from .inline_parser import ESCAPE_CHAR, LINK_LABEL
from .util import unikey
_NEW_LINES = re.compile(r'\r\n|\r')
_BLANK_LINES = re.compile(r'^ +$', re.M)
_TRIM_4 = re.compile(r'^ {1,4}')
_EXPAND_TAB = re.compile(r'^( {0,3})\t', flags=re.M)
_INDENT_CODE_TRIM = re.compile(r'^ {1,4}', flags=re.M)
_BLOCK_QUOTE_TRIM = re.compile(r'^ {0,1}', flags=re.M)
_BLOCK_QUOTE_LEADING = re.compile(r'^ *>', flags=re.M)
_BLOCK_TAGS = {
'address', 'article', 'aside', 'base', 'basefont', 'blockquote',
'body', 'caption', 'center', 'col', 'colgroup', 'dd', 'details',
'dialog', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'iframe',
'legend', 'li', 'link', 'main', 'menu', 'menuitem', 'meta', 'nav',
'noframes', 'ol', 'optgroup', 'option', 'p', 'param', 'section',
'source', 'summary', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
'title', 'tr', 'track', 'ul'
}
_BLOCK_HTML_RULE6 = (
r'</?(?:' + '|'.join(_BLOCK_TAGS) + r')'
r'(?: +|\n|/?>)[\s\S]*?'
r'(?:\n{2,}|\n*$)'
)
_BLOCK_HTML_RULE7 = (
# open tag
r'<(?!script|pre|style)([a-z][\w-]*)(?:'
r' +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"|'
r''' *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?'''
r')*? */?>(?=\s*\n)[\s\S]*?(?:\n{2,}|\n*$)|'
# close tag
r'</(?!script|pre|style)[a-z][\w-]*\s*>(?=\s*\n)[\s\S]*?(?:\n{2,}|\n*$)'
)
_PARAGRAPH_SPLIT = re.compile(r'\n{2,}')
_LIST_BULLET = re.compile(r'^ *([\*\+-]|\d+[.)])')
class BlockParser(ScannerParser):
scanner_cls = Matcher
NEWLINE = re.compile(r'\n+')
DEF_LINK = re.compile(
r' {0,3}\[(' + LINK_LABEL + r')\]:(?:[ \t]*\n)?[ \t]*'
r'<?([^\s>]+)>?(?:[ \t]*\n)?'
r'(?: +["(]([^\n]+)[")])? *\n+'
)
AXT_HEADING = re.compile(
r' {0,3}(#{1,6})(?!#+)(?: *\n+|'
r'\s+([^\n]*?)(?:\n+|\s+?#+\s*\n+))'
)
SETEX_HEADING = re.compile(r'([^\n]+)\n *(=|-){2,}[ \t]*\n+')
THEMATIC_BREAK = re.compile(
r' {0,3}((?:-[ \t]*){3,}|'
r'(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})\n+'
)
INDENT_CODE = re.compile(r'(?:\n*)(?:(?: {4}| *\t)[^\n]+\n*)+')
FENCED_CODE = re.compile(
r'( {0,3})(`{3,}|~{3,})([^`\n]*)\n'
r'(?:|([\s\S]*?)\n)'
r'(?: {0,3}\2[~`]* *\n+|$)'
)
BLOCK_QUOTE = re.compile(
r'(?: {0,3}>[^\n]*\n)+'
)
LIST_START = re.compile(
r'( {0,3})([\*\+-]|\d{1,9}[.)])(?:[ \t]*|[ \t][^\n]+)\n+'
)
BLOCK_HTML = re.compile((
r' {0,3}(?:'
r'<(script|pre|style)[\s>][\s\S]*?(?:</\1>[^\n]*\n+|$)|'
r'<!--(?!-?>)[\s\S]*?-->[^\n]*\n+|'
r'<\?[\s\S]*?\?>[^\n]*\n+|'
r'<![A-Z][\s\S]*?>[^\n]*\n+|'
r'<!\[CDATA\[[\s\S]*?\]\]>[^\n]*\n+'
r'|' + _BLOCK_HTML_RULE6 + '|' + _BLOCK_HTML_RULE7 + ')'
), re.I)
LIST_MAX_DEPTH = 6
BLOCK_QUOTE_MAX_DEPTH = 6
RULE_NAMES = (
'newline', 'thematic_break',
'fenced_code', 'indent_code',
'block_quote', 'block_html',
'list_start',
'axt_heading', 'setex_heading',
'def_link',
)
def __init__(self):
super(BlockParser, self).__init__()
self.block_quote_rules = list(self.RULE_NAMES)
self.list_rules = list(self.RULE_NAMES)
def parse_newline(self, m, state):
return {'type': 'newline', 'blank': True}
def parse_thematic_break(self, m, state):
return {'type': 'thematic_break', 'blank': True}
def parse_indent_code(self, m, state):
text = expand_leading_tab(m.group(0))
code = _INDENT_CODE_TRIM.sub('', text)
code = code.lstrip('\n')
return self.tokenize_block_code(code, None, state)
def parse_fenced_code(self, m, state):
info = ESCAPE_CHAR.sub(r'\1', m.group(3))
spaces = m.group(1)
code = m.group(4) or ''
if spaces and code:
_trim_pattern = re.compile('^' + spaces, re.M)
code = _trim_pattern.sub('', code)
return self.tokenize_block_code(code + '\n', info, state)
def tokenize_block_code(self, code, info, state):
token = {'type': 'block_code', 'raw': code}
if info:
token['params'] = (info, )
return token
def parse_axt_heading(self, m, state):
level = len(m.group(1))
text = m.group(2) or ''
text = text.strip()
if set(text) == {'#'}:
text = ''
return self.tokenize_heading(text, level, state)
def parse_setex_heading(self, m, state):
level = 1 if m.group(2) == '=' else 2
text = m.group(1)
text = text.strip()
return self.tokenize_heading(text, level, state)
def tokenize_heading(self, text, level, state):
return {'type': 'heading', 'text': text, 'params': (level,)}
def get_block_quote_rules(self, depth):
if depth > self.BLOCK_QUOTE_MAX_DEPTH - 1:
rules = list(self.block_quote_rules)
rules.remove('block_quote')
return rules
return self.block_quote_rules
def parse_block_quote(self, m, state):
depth = state.get('block_quote_depth', 0) + 1
state['block_quote_depth'] = depth
# normalize block quote text
text = _BLOCK_QUOTE_LEADING.sub('', m.group(0))
text = expand_leading_tab(text)
text = _BLOCK_QUOTE_TRIM.sub('', text)
text = cleanup_lines(text)
rules = self.get_block_quote_rules(depth)
children = self.parse(text, state, rules)
state['block_quote_depth'] = depth - 1
return {'type': 'block_quote', 'children': children}
def get_list_rules(self, depth):
if depth > self.LIST_MAX_DEPTH - 1:
rules = list(self.list_rules)
rules.remove('list_start')
return rules
return self.list_rules
def parse_list_start(self, m, state, string):
items = []
spaces = m.group(1)
marker = m.group(2)
items, pos = _find_list_items(string, m.start(), spaces, marker)
tight = '\n\n' not in ''.join(items).strip()
ordered = len(marker) != 1
if ordered:
start = int(marker[:-1])
if start == 1:
start = None
else:
start = None
list_tights = state.get('list_tights', [])
list_tights.append(tight)
state['list_tights'] = list_tights
depth = len(list_tights)
rules = self.get_list_rules(depth)
children = [
self.parse_list_item(item, depth, state, rules)
for item in items
]
list_tights.pop()
params = (ordered, depth, start)
token = {'type': 'list', 'children': children, 'params': params}
return token, pos
def parse_list_item(self, text, depth, state, rules):
text = self.normalize_list_item_text(text)
if not text:
children = [{'type': 'block_text', 'text': ''}]
else:
children = self.parse(text, state, rules)
return {
'type': 'list_item',
'params': (depth,),
'children': children,
}
@staticmethod
def normalize_list_item_text(text):
text_length = len(text)
text = _LIST_BULLET.sub('', text)
if not text.strip():
return ''
space = text_length - len(text)
text = expand_leading_tab(text)
if text.startswith(' '):
text = text[1:]
space += 1
else:
text_length = len(text)
text = _TRIM_4.sub('', text)
space += max(text_length - len(text), 1)
# outdent
if '\n ' in text:
pattern = re.compile(r'\n {1,' + str(space) + r'}')
text = pattern.sub(r'\n', text)
return text
def parse_block_html(self, m, state):
html = m.group(0).rstrip()
return {'type': 'block_html', 'raw': html}
def parse_def_link(self, m, state):
key = unikey(m.group(1))
link = m.group(2)
title = m.group(3)
if key not in state['def_links']:
state['def_links'][key] = (link, title)
def parse_text(self, text, state):
list_tights = state.get('list_tights')
if list_tights and list_tights[-1]:
return {'type': 'block_text', 'text': text.strip()}
tokens = []
for s in _PARAGRAPH_SPLIT.split(text):
s = s.strip()
if s:
tokens.append({'type': 'paragraph', 'text': s})
return tokens
def parse(self, s, state, rules=None):
if rules is None:
rules = self.rules
return list(self._scan(s, state, rules))
def render(self, tokens, inline, state):
data = self._iter_render(tokens, inline, state)
return inline.renderer.finalize(data)
def _iter_render(self, tokens, inline, state):
for tok in tokens:
method = inline.renderer._get_method(tok['type'])
if 'blank' in tok:
yield method()
continue
if 'children' in tok:
children = self.render(tok['children'], inline, state)
elif 'raw' in tok:
children = tok['raw']
else:
children = inline(tok['text'], state)
params = tok.get('params')
if params:
yield method(children, *params)
else:
yield method(children)
def cleanup_lines(s):
s = _NEW_LINES.sub('\n', s)
s = _BLANK_LINES.sub('', s)
return s
def expand_leading_tab(text):
return _EXPAND_TAB.sub(_expand_tab_repl, text)
def _expand_tab_repl(m):
s = m.group(1)
return s + ' ' * (4 - len(s))
def _create_list_item_pattern(spaces, marker):
prefix = r'( {0,' + str(len(spaces) + len(marker)) + r'})'
if len(marker) > 1:
if marker[-1] == '.':
prefix = prefix + r'\d{0,9}\.'
else:
prefix = prefix + r'\d{0,9}\)'
else:
if marker == '*':
prefix = prefix + r'\*'
elif marker == '+':
prefix = prefix + r'\+'
else:
prefix = prefix + r'-'
s1 = ' {' + str(len(marker) + 1) + ',}'
if len(marker) > 4:
s2 = ' {' + str(len(marker) - 4) + r',}\t'
else:
s2 = r' *\t'
return re.compile(
prefix + r'(?:[ \t]*|[ \t]+[^\n]+)\n+'
r'(?:\1(?:' + s1 + '|' + s2 + ')'
r'[^\n]+\n+)*'
)
def _find_list_items(string, pos, spaces, marker):
items = []
if marker in {'*', '-'}:
is_hr = re.compile(
r' *((?:-[ \t]*){3,}|(?:\*[ \t]*){3,})\n+'
)
else:
is_hr = None
pattern = _create_list_item_pattern(spaces, marker)
while 1:
m = pattern.match(string, pos)
if not m:
break
text = m.group(0)
if is_hr and is_hr.match(text):
break
new_spaces = m.group(1)
if new_spaces != spaces:
spaces = new_spaces
pattern = _create_list_item_pattern(spaces, marker)
items.append(text)
pos = m.end()
return items, pos

View File

@ -0,0 +1,10 @@
from .base import Directive
from .admonition import Admonition
from .include import DirectiveInclude
from .toc import DirectiveToc, extract_toc_items, render_toc_ul
__all__ = [
'Directive', 'Admonition', 'DirectiveInclude',
'DirectiveToc', 'extract_toc_items', 'render_toc_ul',
]

View File

@ -0,0 +1,57 @@
from .base import Directive
class Admonition(Directive):
SUPPORTED_NAMES = {
"attention", "caution", "danger", "error", "hint",
"important", "note", "tip", "warning",
}
def parse(self, block, m, state):
options = self.parse_options(m)
if options:
return {
'type': 'block_error',
'raw': 'Admonition has no options'
}
name = m.group('name')
title = m.group('value')
text = self.parse_text(m)
rules = list(block.rules)
rules.remove('directive')
children = block.parse(text, state, rules)
return {
'type': 'admonition',
'children': children,
'params': (name, title)
}
def __call__(self, md):
for name in self.SUPPORTED_NAMES:
self.register_directive(md, name)
if md.renderer.NAME == 'html':
md.renderer.register('admonition', render_html_admonition)
elif md.renderer.NAME == 'ast':
md.renderer.register('admonition', render_ast_admonition)
def render_html_admonition(text, name, title=""):
html = '<section class="admonition ' + name + '">\n'
if not title:
title = name.capitalize()
if title:
html += '<p class="admonition-title">' + title + '</p>\n'
if text:
html += text
return html + '</section>\n'
def render_ast_admonition(children, name, title=""):
return {
'type': 'admonition',
'children': children,
'name': name,
'title': title,
}

View File

@ -0,0 +1,99 @@
"""
Directive Syntax
~~~~~~~~~~~~~~~~~
This syntax is inspired by reStructuredText. The syntax is very powerful,
that you can define a lot of custom features by your own.
The syntax looks like::
.. directive-name:: directive value
:option-key: option value
:option-key: option value
full featured markdown text here
:copyright: (c) Hsiaoming Yang
"""
import re
__all__ = ['Directive']
DIRECTIVE_PATTERN = re.compile(
r'\.\.( +)(?P<name>[a-zA-Z0-9_-]+)\:\: *(?P<value>[^\n]*)\n+'
r'(?P<options>(?: \1 {0,3}\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)'
r'(?P<text>(?: \1 {0,3}[^\n]*\n+)*)'
)
class Directive(object):
@staticmethod
def parse_text(m):
text = m.group('text')
if not text.strip():
return ''
leading = len(m.group(1)) + 2
text = '\n'.join(line[leading:] for line in text.splitlines())
return text.lstrip('\n') + '\n'
@staticmethod
def parse_options(m):
text = m.group('options')
if not text.strip():
return []
options = []
for line in re.split(r'\n+', text):
line = line.strip()[1:]
if not line:
continue
i = line.find(':')
k = line[:i]
v = line[i + 1:].strip()
options.append((k, v))
return options
def register_directive(self, md, name):
plugin = getattr(md, '_directive', None)
if not plugin:
plugin = PluginDirective()
plugin(md)
plugin.register_directive(name, self.parse)
def parse(self, block, m, state):
raise NotImplementedError()
def __call__(self, md):
raise NotImplementedError()
class PluginDirective(object):
def __init__(self):
self._directives = {}
def register_directive(self, name, fn):
self._directives[name] = fn
def parse_block_directive(self, block, m, state):
name = m.group('name')
method = self._directives.get(name)
if method:
return method(block, m, state)
token = {
'type': 'block_error',
'raw': 'Unsupported directive: ' + name,
}
return token
def __call__(self, md):
md._directive = self
md.block.register_rule(
'directive', DIRECTIVE_PATTERN,
self.parse_block_directive
)
md.block.rules.append('directive')

View File

@ -0,0 +1,71 @@
import os
from mistune.markdown import preprocess
from .base import Directive
class DirectiveInclude(Directive):
def parse(self, block, m, state):
source_file = state.get('__file__')
if not source_file:
return {
'type': 'block_error',
'raw': 'Missing source file configuration',
}
relpath = m.group('value')
options = self.parse_options(m)
dest = os.path.join(os.path.dirname(source_file), relpath)
dest = os.path.normpath(dest)
if dest == source_file:
return {
'type': 'block_error',
'raw': 'Could not include self: ' + relpath,
}
if not os.path.isfile(dest):
return {
'type': 'block_error',
'raw': 'Could not find file: ' + relpath,
}
with open(dest, 'rb') as f:
content = f.read()
text = content.decode('utf-8')
if not options:
ext = os.path.splitext(relpath)[1]
if ext in {'.md', '.markdown', '.mkd'}:
text, state = preprocess(text, {'__file__': dest})
return block.parse(text, state)
if ext in {'.html', '.xhtml', '.htm'}:
return {'type': 'block_html', 'text': text}
return {
'type': 'include',
'raw': text,
'params': (relpath, dest, options)
}
def __call__(self, md):
self.register_directive(md, 'include')
if md.renderer.NAME == 'html':
md.renderer.register('include', render_html_include)
elif md.renderer.NAME == 'ast':
md.renderer.register('include', render_ast_include)
def render_ast_include(text, relpath, abspath=None, options=None):
return {
'type': 'include',
'text': text,
'relpath': relpath,
'abspath': abspath,
'options': options,
}
def render_html_include(text, relpath, abspath=None, options=None):
html = '<section class="directive-include" data-relpath="'
return html + relpath + '">\n' + text + '</section>\n'

View File

@ -0,0 +1,215 @@
"""
TOC directive
~~~~~~~~~~~~~
The TOC directive syntax looks like::
.. toc:: Title
:depth: 3
"Title" and "depth" option can be empty. "depth" is an integer less
than 6, which defines the max heading level writers want to include
in TOC.
"""
from .base import Directive
class DirectiveToc(Directive):
def __init__(self, depth=3):
self.depth = depth
def parse(self, block, m, state):
title = m.group('value')
depth = None
options = self.parse_options(m)
if options:
depth = dict(options).get('depth')
if depth:
try:
depth = int(depth)
except (ValueError, TypeError):
return {
'type': 'block_error',
'raw': 'TOC depth MUST be integer',
}
return {'type': 'toc', 'raw': None, 'params': (title, depth)}
def reset_toc_state(self, md, s, state):
state['toc_depth'] = self.depth
state['toc_headings'] = []
return s, state
def register_plugin(self, md):
md.block.tokenize_heading = record_toc_heading
md.before_parse_hooks.append(self.reset_toc_state)
md.before_render_hooks.append(md_toc_hook)
if md.renderer.NAME == 'html':
md.renderer.register('theading', render_html_theading)
elif md.renderer.NAME == 'ast':
md.renderer.register('theading', render_ast_theading)
def __call__(self, md):
self.register_directive(md, 'toc')
self.register_plugin(md)
if md.renderer.NAME == 'html':
md.renderer.register('toc', render_html_toc)
elif md.renderer.NAME == 'ast':
md.renderer.register('toc', render_ast_toc)
def record_toc_heading(text, level, state):
# we will use this method to replace tokenize_heading
tid = 'toc_' + str(len(state['toc_headings']) + 1)
state['toc_headings'].append((tid, text, level))
return {'type': 'theading', 'text': text, 'params': (level, tid)}
def md_toc_hook(md, tokens, state):
headings = state.get('toc_headings')
if not headings:
return tokens
# add TOC items into the given location
default_depth = state.get('toc_depth', 3)
headings = list(_cleanup_headings_text(md.inline, headings, state))
for tok in tokens:
if tok['type'] == 'toc':
params = tok['params']
depth = params[1] or default_depth
items = [d for d in headings if d[2] <= depth]
tok['raw'] = items
return tokens
def render_ast_toc(items, title, depth):
return {
'type': 'toc',
'items': [list(d) for d in items],
'title': title,
'depth': depth,
}
def render_ast_theading(children, level, tid):
return {
'type': 'heading', 'children': children,
'level': level, 'id': tid,
}
def render_html_toc(items, title, depth):
html = '<section class="toc">\n'
if title:
html += '<h1>' + title + '</h1>\n'
return html + render_toc_ul(items) + '</section>\n'
def render_html_theading(text, level, tid):
tag = 'h' + str(level)
return '<' + tag + ' id="' + tid + '">' + text + '</' + tag + '>\n'
def extract_toc_items(md, s):
"""Extract TOC headings into list structure of::
[
('toc_1', 'Introduction', 1),
('toc_2', 'Install', 2),
('toc_3', 'Upgrade', 2),
('toc_4', 'License', 1),
]
:param md: Markdown Instance with TOC plugin.
:param s: text string.
"""
s, state = md.before_parse(s, {})
md.block.parse(s, state)
headings = state.get('toc_headings')
if not headings:
return []
return list(_cleanup_headings_text(md.inline, headings, state))
def render_toc_ul(toc):
"""Render a <ul> table of content HTML. The param "toc" should
be formatted into this structure::
[
(toc_id, text, level),
]
For example::
[
('toc-intro', 'Introduction', 1),
('toc-install', 'Install', 2),
('toc-upgrade', 'Upgrade', 2),
('toc-license', 'License', 1),
]
"""
if not toc:
return ''
s = '<ul>\n'
levels = []
for k, text, level in toc:
item = '<a href="#{}">{}</a>'.format(k, text)
if not levels:
s += '<li>' + item
levels.append(level)
elif level == levels[-1]:
s += '</li>\n<li>' + item
elif level > levels[-1]:
s += '\n<ul>\n<li>' + item
levels.append(level)
else:
last_level = levels.pop()
while levels:
last_level = levels.pop()
if level == last_level:
s += '</li>\n</ul>\n</li>\n<li>' + item
levels.append(level)
break
elif level > last_level:
s += '</li>\n<li>' + item
levels.append(last_level)
levels.append(level)
break
else:
s += '</li>\n</ul>\n'
else:
levels.append(level)
s += '</li>\n<li>' + item
while len(levels) > 1:
s += '</li>\n</ul>\n'
levels.pop()
return s + '</li>\n</ul>\n'
def _cleanup_headings_text(inline, items, state):
for item in items:
text = item[1]
tokens = inline._scan(text, state, inline.rules)
text = ''.join(_inline_token_text(tok) for tok in tokens)
yield item[0], text, item[2]
def _inline_token_text(token):
tok_type = token[0]
if tok_type == 'inline_html':
return ''
if len(token) == 2:
return token[1]
if tok_type in {'image', 'link'}:
return token[2]
return ''

View File

@ -0,0 +1,220 @@
import re
from .scanner import ScannerParser
from .util import PUNCTUATION, ESCAPE_TEXT, escape_url, unikey
HTML_TAGNAME = r'[A-Za-z][A-Za-z0-9-]*'
HTML_ATTRIBUTES = (
r'(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*'
r'(?:\s*=\s*(?:[^ "\'=<>`]+|\'[^\']*?\'|"[^\"]*?"))?)*'
)
ESCAPE_CHAR = re.compile(r'\\([' + PUNCTUATION + r'])')
LINK_TEXT = r'(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?'
LINK_LABEL = r'(?:[^\\\[\]]|' + ESCAPE_TEXT + r'){0,1000}'
class InlineParser(ScannerParser):
ESCAPE = ESCAPE_TEXT
#: link or email syntax::
#:
#: <https://example.com>
AUTO_LINK = (
r'(?<!\\)(?:\\\\)*<([A-Za-z][A-Za-z0-9+.-]{1,31}:'
r"[^ <>]*?|[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9]"
r'(?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
r'(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)>'
)
#: link or image syntax::
#:
#: [text](/link "title")
#: ![alt](/src "title")
STD_LINK = (
r'!?\[(' + LINK_TEXT + r')\]\(\s*'
r'(<(?:\\[<>]?|[^\s<>\\])*>|'
r'(?:\\[()]?|\([^\s\x00-\x1f\\]*\)|[^\s\x00-\x1f()\\])*?)'
r'(?:\s+('
r'''"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)'''
r'))?\s*\)'
)
#: Get link from references. References are defined in DEF_LINK in blocks.
#: The syntax looks like::
#:
#: [an example][id]
#:
#: [id]: https://example.com "optional title"
REF_LINK = (
r'!?\[(' + LINK_TEXT + r')\]'
r'\[(' + LINK_LABEL + r')\]'
)
#: Simple form of reference link::
#:
#: [an example]
#:
#: [an example]: https://example.com "optional title"
REF_LINK2 = r'!?\[(' + LINK_LABEL + r')\]'
#: emphasis and strong * or _::
#:
#: *emphasis* **strong**
#: _emphasis_ __strong__
ASTERISK_EMPHASIS = (
r'(\*{1,2})(?=[^\s*])('
r'(?:(?:(?<!\\)(?:\\\\)*\*)|[^*])+?'
r')(?<!\\)\1'
)
UNDERSCORE_EMPHASIS = (
r'\b(_{1,2})(?=[^\s_])([\s\S]*?'
r'(?:' + ESCAPE_TEXT + r'|[^\s_]))\1'
r'(?!_|[^\s' + PUNCTUATION + r'])\b'
)
#: codespan with `::
#:
#: `code`
CODESPAN = (
r'(?<!\\|`)(?:\\\\)*(`+)(?!`)([\s\S]+?)(?<!`)\1(?!`)'
)
#: linebreak leaves two spaces at the end of line
LINEBREAK = r'(?:\\| {2,})\n(?!\s*$)'
INLINE_HTML = (
r'(?<!\\)<' + HTML_TAGNAME + HTML_ATTRIBUTES + r'\s*/?>|' # open tag
r'(?<!\\)</' + HTML_TAGNAME + r'\s*>|' # close tag
r'(?<!\\)<!--(?!>|->)(?:(?!--)[\s\S])+?(?<!-)-->|' # comment
r'(?<!\\)<\?[\s\S]+?\?>|'
r'(?<!\\)<![A-Z][\s\S]+?>|' # doctype
r'(?<!\\)<!\[CDATA[\s\S]+?\]\]>' # cdata
)
RULE_NAMES = (
'escape', 'inline_html', 'auto_link',
'std_link', 'ref_link', 'ref_link2',
'asterisk_emphasis', 'underscore_emphasis',
'codespan', 'linebreak',
)
def __init__(self, renderer, hard_wrap=False):
super(InlineParser, self).__init__()
if hard_wrap:
#: every new line becomes <br>
self.LINEBREAK = r' *\n(?!\s*$)'
self.renderer = renderer
rules = list(self.RULE_NAMES)
rules.remove('ref_link')
rules.remove('ref_link2')
self.ref_link_rules = rules
def parse_escape(self, m, state):
text = m.group(0)[1:]
return 'text', text
def parse_auto_link(self, m, state):
if state.get('_in_link'):
return 'text', m.group(0)
text = m.group(1)
schemes = ('mailto:', 'http://', 'https://')
if '@' in text and not text.lower().startswith(schemes):
link = 'mailto:' + text
else:
link = text
return 'link', escape_url(link), text
def parse_std_link(self, m, state):
line = m.group(0)
text = m.group(1)
link = ESCAPE_CHAR.sub(r'\1', m.group(2))
if link.startswith('<') and link.endswith('>'):
link = link[1:-1]
title = m.group(3)
if title:
title = ESCAPE_CHAR.sub(r'\1', title[1:-1])
if line[0] == '!':
return 'image', escape_url(link), text, title
return self.tokenize_link(line, link, text, title, state)
def parse_ref_link(self, m, state):
line = m.group(0)
text = m.group(1)
key = unikey(m.group(2) or text)
def_links = state.get('def_links')
if not def_links or key not in def_links:
return list(self._scan(line, state, self.ref_link_rules))
link, title = def_links.get(key)
link = ESCAPE_CHAR.sub(r'\1', link)
if title:
title = ESCAPE_CHAR.sub(r'\1', title)
if line[0] == '!':
return 'image', escape_url(link), text, title
return self.tokenize_link(line, link, text, title, state)
def parse_ref_link2(self, m, state):
return self.parse_ref_link(m, state)
def tokenize_link(self, line, link, text, title, state):
if state.get('_in_link'):
return 'text', line
state['_in_link'] = True
text = self.render(text, state)
state['_in_link'] = False
return 'link', escape_url(link), text, title
def parse_asterisk_emphasis(self, m, state):
return self.tokenize_emphasis(m, state)
def parse_underscore_emphasis(self, m, state):
return self.tokenize_emphasis(m, state)
def tokenize_emphasis(self, m, state):
marker = m.group(1)
text = m.group(2)
if len(marker) == 1:
return 'emphasis', self.render(text, state)
return 'strong', self.render(text, state)
def parse_codespan(self, m, state):
code = re.sub(r'[ \n]+', ' ', m.group(2).strip())
return 'codespan', code
def parse_linebreak(self, m, state):
return 'linebreak',
def parse_inline_html(self, m, state):
html = m.group(0)
if html.startswith('<a '):
state['_in_link'] = True
if html.startswith('</a>'):
state['_in_link'] = False
return 'inline_html', html
def parse_text(self, text, state):
return 'text', text
def parse(self, s, state, rules=None):
if rules is None:
rules = self.rules
tokens = (
self.renderer._get_method(t[0])(*t[1:])
for t in self._scan(s, state, rules)
)
return tokens
def render(self, s, state, rules=None):
tokens = self.parse(s, state, rules)
return self.renderer.finalize(tokens)
def __call__(self, s, state):
return self.render(s, state)

View File

@ -0,0 +1,84 @@
from .block_parser import BlockParser, expand_leading_tab, cleanup_lines
from .inline_parser import InlineParser
class Markdown(object):
def __init__(self, renderer, block=None, inline=None, plugins=None):
if block is None:
block = BlockParser()
if inline is None:
inline = InlineParser(renderer)
self.block = block
self.inline = inline
self.renderer = inline.renderer
self.before_parse_hooks = []
self.before_render_hooks = []
self.after_render_hooks = []
if plugins:
for plugin in plugins:
plugin(self)
def use(self, plugin):
plugin(self)
def before_parse(self, s, state):
s, state = preprocess(s, state)
for hook in self.before_parse_hooks:
s, state = hook(self, s, state)
return s, state
def before_render(self, tokens, state):
for hook in self.before_render_hooks:
tokens = hook(self, tokens, state)
return tokens
def after_render(self, result, state):
for hook in self.after_render_hooks:
result = hook(self, result, state)
return result
def parse(self, s, state=None):
if state is None:
state = {}
s, state = self.before_parse(s, state)
tokens = self.block.parse(s, state)
tokens = self.before_render(tokens, state)
result = self.block.render(tokens, self.inline, state)
result = self.after_render(result, state)
return result
def read(self, filepath, state=None):
if state is None:
state = {}
state['__file__'] = filepath
with open(filepath, 'rb') as f:
s = f.read()
return self.parse(s.decode('utf-8'), state)
def __call__(self, s):
return self.parse(s)
def preprocess(s, state):
state.update({
'def_links': {},
'def_footnotes': {},
'footnotes': [],
})
if s is None:
s = '\n'
else:
s = s.replace('\u2424', '\n')
s = cleanup_lines(s)
s = expand_leading_tab(s)
if not s.endswith('\n'):
s += '\n'
return s, state

View File

@ -0,0 +1,25 @@
from .abbr import plugin_abbr
from .def_list import plugin_def_list
from .extra import plugin_strikethrough, plugin_url
from .footnotes import plugin_footnotes
from .table import plugin_table
from .task_lists import plugin_task_lists
PLUGINS = {
"url": plugin_url,
"strikethrough": plugin_strikethrough,
"footnotes": plugin_footnotes,
"table": plugin_table,
"task_lists": plugin_task_lists,
"def_list": plugin_def_list,
"abbr": plugin_abbr,
}
__all__ = [
"PLUGINS",
"plugin_url",
"plugin_strikethrough",
"plugin_footnotes",
"plugin_table",
"plugin_abbr",
]

View File

@ -0,0 +1,63 @@
import re
from ..util import escape_html
DEF_ABBR = re.compile(
# *[HTML]:
# *[HTML]: Hyper Text Markup Language
# *[HTML]:
# Hyper Text Markup Language
r'\*\[([^\]]+)\]:'
r'((?:[ \t]*\n(?: {3,}|\t)[^\n]+)|(?:[^\n]*))\n*'
)
def parse_def_abbr(block, m, state):
def_abbrs = state.get('def_abbrs', {})
label = m.group(1)
definition = m.group(2)
def_abbrs[label] = definition.strip()
state['def_abbrs'] = def_abbrs
def parse_inline_abbr(inline, m, state):
def_abbrs = state['def_abbrs']
label = m.group(0)
return 'abbr', label, def_abbrs[label]
def after_parse_def_abbr(md, tokens, state):
def_abbrs = state.get('def_abbrs')
if def_abbrs:
labels = list(def_abbrs.keys())
abbr_pattern = r'|'.join(re.escape(k) for k in labels)
md.inline.register_rule('abbr', abbr_pattern, parse_inline_abbr)
md.inline.rules.append('abbr')
return tokens
def render_html_abbr(key, definition):
title_attribute = ""
if definition:
definition = escape_html(definition)
title_attribute = ' title="{}"'.format(definition)
return "<abbr{title_attribute}>{key}</abbr>".format(
key=key,
title_attribute=title_attribute,
)
def render_ast_abbr(key, definition):
return {'type': 'abbr', 'text': key, 'definition': definition}
def plugin_abbr(md):
md.block.register_rule('def_abbr', DEF_ABBR, parse_def_abbr)
md.before_render_hooks.append(after_parse_def_abbr)
md.block.rules.append('def_abbr')
if md.renderer.NAME == 'html':
md.renderer.register('abbr', render_html_abbr)
elif md.renderer.NAME == 'ast':
md.renderer.register('abbr', render_ast_abbr)

View File

@ -0,0 +1,54 @@
import re
__all__ = ["plugin_def_list"]
DEFINITION_LIST_PATTERN = re.compile(r"([^\n]+\n(:[ \t][^\n]+\n)+\n?)+")
def parse_def_list(block, m, state):
lines = m.group(0).split("\n")
definition_list_items = []
for line in lines:
if not line:
continue
if line.strip()[0] == ":":
definition_list_items.append(
{"type": "def_list_item", "text": line[1:].strip()}
)
else:
definition_list_items.append(
{"type": "def_list_header", "text": line.strip()}
)
return {"type": "def_list", "children": definition_list_items}
def render_html_def_list(text):
return "<dl>\n" + text + "</dl>\n"
def render_html_def_list_header(text):
return "<dt>" + text + "</dt>\n"
def render_html_def_list_item(text):
return "<dd>" + text + "</dd>\n"
def render_ast_def_list_header(text):
return {"type": "def_list_header", "text": text[0]["text"]}
def render_ast_def_list_item(text):
return {"type": "def_list_item", "text": text[0]["text"]}
def plugin_def_list(md):
md.block.register_rule("def_list", DEFINITION_LIST_PATTERN, parse_def_list)
md.block.rules.append("def_list")
if md.renderer.NAME == "html":
md.renderer.register("def_list", render_html_def_list)
md.renderer.register("def_list_header", render_html_def_list_header)
md.renderer.register("def_list_item", render_html_def_list_item)
if md.renderer.NAME == "ast":
md.renderer.register("def_list_header", render_ast_def_list_header)
md.renderer.register("def_list_item", render_ast_def_list_item)

View File

@ -0,0 +1,50 @@
from ..util import escape_url, ESCAPE_TEXT
__all__ = ['plugin_url', 'plugin_strikethrough']
#: url link like: ``https://lepture.com/``
URL_LINK_PATTERN = r'''(https?:\/\/[^\s<]+[^<.,:;"')\]\s])'''
def parse_url_link(inline, m, state):
url = m.group(0)
if state.get('_in_link'):
return 'text', url
return 'link', escape_url(url)
def plugin_url(md):
md.inline.register_rule('url_link', URL_LINK_PATTERN, parse_url_link)
md.inline.rules.append('url_link')
#: strike through syntax looks like: ``~~word~~``
STRIKETHROUGH_PATTERN = (
r'~~(?=[^\s~])('
r'(?:\\~|[^~])*'
r'(?:' + ESCAPE_TEXT + r'|[^\s~]))~~'
)
def parse_strikethrough(inline, m, state):
text = m.group(1)
return 'strikethrough', inline.render(text, state)
def render_html_strikethrough(text):
return '<del>' + text + '</del>'
def plugin_strikethrough(md):
md.inline.register_rule(
'strikethrough', STRIKETHROUGH_PATTERN, parse_strikethrough)
index = md.inline.rules.index('codespan')
if index != -1:
md.inline.rules.insert(index + 1, 'strikethrough')
else: # pragma: no cover
md.inline.rules.append('strikethrough')
if md.renderer.NAME == 'html':
md.renderer.register('strikethrough', render_html_strikethrough)

View File

@ -0,0 +1,149 @@
import re
from ..inline_parser import LINK_LABEL
from ..util import unikey
__all__ = ['plugin_footnotes']
#: inline footnote syntax looks like::
#:
#: [^key]
INLINE_FOOTNOTE_PATTERN = r'\[\^(' + LINK_LABEL + r')\]'
#: define a footnote item like::
#:
#: [^key]: paragraph text to describe the note
DEF_FOOTNOTE = re.compile(
r'( {0,3})\[\^(' + LINK_LABEL + r')\]:[ \t]*('
r'[^\n]*\n+'
r'(?:\1 {1,3}(?! )[^\n]*\n+)*'
r')'
)
def parse_inline_footnote(inline, m, state):
key = unikey(m.group(1))
def_footnotes = state.get('def_footnotes')
if not def_footnotes or key not in def_footnotes:
return 'text', m.group(0)
index = state.get('footnote_index', 0)
index += 1
state['footnote_index'] = index
state['footnotes'].append(key)
return 'footnote_ref', key, index
def parse_def_footnote(block, m, state):
key = unikey(m.group(2))
if key not in state['def_footnotes']:
state['def_footnotes'][key] = m.group(3)
def parse_footnote_item(block, k, i, state):
def_footnotes = state['def_footnotes']
text = def_footnotes[k]
stripped_text = text.strip()
if '\n' not in stripped_text:
children = [{'type': 'paragraph', 'text': stripped_text}]
else:
lines = text.splitlines()
for second_line in lines[1:]:
if second_line:
break
spaces = len(second_line) - len(second_line.lstrip())
pattern = re.compile(r'^ {' + str(spaces) + r',}', flags=re.M)
text = pattern.sub('', text)
children = block.parse_text(text, state)
if not isinstance(children, list):
children = [children]
return {
'type': 'footnote_item',
'children': children,
'params': (k, i)
}
def md_footnotes_hook(md, result, state):
footnotes = state.get('footnotes')
if not footnotes:
return result
children = [
parse_footnote_item(md.block, k, i + 1, state)
for i, k in enumerate(footnotes)
]
tokens = [{'type': 'footnotes', 'children': children}]
output = md.block.render(tokens, md.inline, state)
return result + output
def render_ast_footnote_ref(key, index):
return {'type': 'footnote_ref', 'key': key, 'index': index}
def render_ast_footnote_item(children, key, index):
return {
'type': 'footnote_item',
'children': children,
'key': key,
'index': index,
}
def render_html_footnote_ref(key, index):
i = str(index)
html = '<sup class="footnote-ref" id="fnref-' + i + '">'
return html + '<a href="#fn-' + i + '">' + i + '</a></sup>'
def render_html_footnotes(text):
return (
'<section class="footnotes">\n<ol>\n'
+ text +
'</ol>\n</section>\n'
)
def render_html_footnote_item(text, key, index):
i = str(index)
back = '<a href="#fnref-' + i + '" class="footnote">&#8617;</a>'
text = text.rstrip()
if text.endswith('</p>'):
text = text[:-4] + back + '</p>'
else:
text = text + back
return '<li id="fn-' + i + '">' + text + '</li>\n'
def plugin_footnotes(md):
md.inline.register_rule(
'footnote',
INLINE_FOOTNOTE_PATTERN,
parse_inline_footnote
)
index = md.inline.rules.index('std_link')
if index != -1:
md.inline.rules.insert(index, 'footnote')
else:
md.inline.rules.append('footnote')
md.block.register_rule('def_footnote', DEF_FOOTNOTE, parse_def_footnote)
index = md.block.rules.index('def_link')
if index != -1:
md.block.rules.insert(index, 'def_footnote')
else:
md.block.rules.append('def_footnote')
if md.renderer.NAME == 'html':
md.renderer.register('footnote_ref', render_html_footnote_ref)
md.renderer.register('footnote_item', render_html_footnote_item)
md.renderer.register('footnotes', render_html_footnotes)
elif md.renderer.NAME == 'ast':
md.renderer.register('footnote_ref', render_ast_footnote_ref)
md.renderer.register('footnote_item', render_ast_footnote_item)
md.after_render_hooks.append(md_footnotes_hook)

View File

@ -0,0 +1,162 @@
import re
__all__ = ['plugin_table']
TABLE_PATTERN = re.compile(
r' {0,3}\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*'
)
NP_TABLE_PATTERN = re.compile(
r' {0,3}(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*'
)
HEADER_SUB = re.compile(r'\| *$')
HEADER_SPLIT = re.compile(r' *\| *')
ALIGN_SPLIT = re.compile(r' *\| *')
def parse_table(self, m, state):
header = HEADER_SUB.sub('', m.group(1)).strip()
align = HEADER_SUB.sub('', m.group(2))
thead, aligns = _process_table(header, align)
text = re.sub(r'(?: *\| *)?\n$', '', m.group(3))
rows = []
for i, v in enumerate(text.split('\n')):
v = re.sub(r'^ *\| *| *\| *$', '', v)
rows.append(_process_row(v, aligns))
children = [thead, {'type': 'table_body', 'children': rows}]
return {'type': 'table', 'children': children}
def parse_nptable(self, m, state):
thead, aligns = _process_table(m.group(1), m.group(2))
text = re.sub(r'\n$', '', m.group(3))
rows = []
for i, v in enumerate(text.split('\n')):
rows.append(_process_row(v, aligns))
children = [thead, {'type': 'table_body', 'children': rows}]
return {'type': 'table', 'children': children}
def _process_table(header, align):
headers = HEADER_SPLIT.split(header)
aligns = ALIGN_SPLIT.split(align)
if header.endswith('|'):
headers.append('')
cells = []
for i, v in enumerate(aligns):
if re.search(r'^ *-+: *$', v):
aligns[i] = 'right'
elif re.search(r'^ *:-+: *$', v):
aligns[i] = 'center'
elif re.search(r'^ *:-+ *$', v):
aligns[i] = 'left'
else:
aligns[i] = None
if len(headers) > i:
cells.append({
'type': 'table_cell',
'text': headers[i],
'params': (aligns[i], True)
})
i += 1
while i + 1 < len(headers):
cells.append({
'type': 'table_cell',
'text': headers[i],
'params': (None, True)
})
aligns.append(None)
i += 1
thead = {'type': 'table_head', 'children': cells}
return thead, aligns
def _process_row(row, aligns):
cells = []
for i, s in enumerate(re.split(r' *(?<!\\)\| *', row)):
text = re.sub(r'\\\|', '|', s.strip())
if len(aligns) < i + 1:
cells.append({
'type': 'table_cell',
'text': text,
'params': (None, False)
})
else:
cells.append({
'type': 'table_cell',
'text': text,
'params': (aligns[i], False)
})
if len(cells) < len(aligns):
for align in aligns[len(cells):]:
cells.append({
'type': 'table_cell',
'text': '',
'params': (align, False),
})
return {'type': 'table_row', 'children': cells}
def render_html_table(text):
return '<table>\n' + text + '</table>\n'
def render_html_table_head(text):
return '<thead>\n<tr>\n' + text + '</tr>\n</thead>\n'
def render_html_table_body(text):
return '<tbody>\n' + text + '</tbody>\n'
def render_html_table_row(text):
return '<tr>\n' + text + '</tr>\n'
def render_html_table_cell(text, align=None, is_head=False):
if is_head:
tag = 'th'
else:
tag = 'td'
html = ' <' + tag
if align:
html += ' style="text-align:' + align + '"'
return html + '>' + text + '</' + tag + '>\n'
def render_ast_table_cell(children, align=None, is_head=False):
return {
'type': 'table_cell',
'children': children,
'align': align,
'is_head': is_head
}
def plugin_table(md):
md.block.register_rule('table', TABLE_PATTERN, parse_table)
md.block.register_rule('nptable', NP_TABLE_PATTERN, parse_nptable)
md.block.rules.append('table')
md.block.rules.append('nptable')
if md.renderer.NAME == 'html':
md.renderer.register('table', render_html_table)
md.renderer.register('table_head', render_html_table_head)
md.renderer.register('table_body', render_html_table_body)
md.renderer.register('table_row', render_html_table_row)
md.renderer.register('table_cell', render_html_table_cell)
elif md.renderer.NAME == 'ast':
md.renderer.register('table_cell', render_ast_table_cell)

View File

@ -0,0 +1,75 @@
import re
__all__ = ['plugin_task_lists']
TASK_LIST_ITEM = re.compile(r'^(\[[ xX]\])\s+')
def task_lists_hook(md, tokens, state):
return _rewrite_all_list_items(tokens)
def render_ast_task_list_item(children, level, checked):
return {
'type': 'task_list_item',
'children': children,
'level': level,
'checked': checked,
}
def render_html_task_list_item(text, level, checked):
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 plugin_task_lists(md):
md.before_render_hooks.append(task_lists_hook)
if md.renderer.NAME == 'html':
md.renderer.register('task_list_item', render_html_task_list_item)
elif md.renderer.NAME == 'ast':
md.renderer.register('task_list_item', render_ast_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.keys():
_rewrite_all_list_items(tok['children'])
return tokens
def _rewrite_list_item(item):
children = item['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():]
params = item['params']
if mark == '[ ]':
params = (params[0], False)
else:
params = (params[0], True)
item['type'] = 'task_list_item'
item['params'] = params

View File

@ -0,0 +1,220 @@
from .util import escape, escape_html
class BaseRenderer(object):
NAME = 'base'
def __init__(self):
self._methods = {}
def register(self, name, method):
self._methods[name] = method
def _get_method(self, name):
try:
return object.__getattribute__(self, name)
except AttributeError:
method = self._methods.get(name)
if not method:
raise AttributeError('No renderer "{!r}"'.format(name))
return method
def finalize(self, data):
raise NotImplementedError(
'The renderer needs to implement the finalize method.')
class AstRenderer(BaseRenderer):
NAME = 'ast'
def text(self, text):
return {'type': 'text', 'text': text}
def link(self, link, children=None, title=None):
if isinstance(children, str):
children = [{'type': 'text', 'text': children}]
return {
'type': 'link',
'link': link,
'children': children,
'title': title,
}
def image(self, src, alt="", title=None):
return {'type': 'image', 'src': src, 'alt': alt, 'title': title}
def codespan(self, text):
return {'type': 'codespan', 'text': text}
def linebreak(self):
return {'type': 'linebreak'}
def inline_html(self, html):
return {'type': 'inline_html', 'text': html}
def heading(self, children, level):
return {'type': 'heading', 'children': children, 'level': level}
def newline(self):
return {'type': 'newline'}
def thematic_break(self):
return {'type': 'thematic_break'}
def block_code(self, children, info=None):
return {
'type': 'block_code',
'text': children,
'info': info
}
def block_html(self, children):
return {'type': 'block_html', 'text': children}
def list(self, children, ordered, level, start=None):
token = {
'type': 'list',
'children': children,
'ordered': ordered,
'level': level,
}
if start is not None:
token['start'] = start
return token
def list_item(self, children, level):
return {'type': 'list_item', 'children': children, 'level': level}
def _create_default_method(self, name):
def __ast(children):
return {'type': name, 'children': children}
return __ast
def _get_method(self, name):
try:
return super(AstRenderer, self)._get_method(name)
except AttributeError:
return self._create_default_method(name)
def finalize(self, data):
return list(data)
class HTMLRenderer(BaseRenderer):
NAME = 'html'
HARMFUL_PROTOCOLS = {
'javascript:',
'vbscript:',
'data:',
}
def __init__(self, escape=True, allow_harmful_protocols=None):
super(HTMLRenderer, self).__init__()
self._escape = escape
self._allow_harmful_protocols = allow_harmful_protocols
def _safe_url(self, url):
if self._allow_harmful_protocols is None:
schemes = self.HARMFUL_PROTOCOLS
elif self._allow_harmful_protocols is True:
schemes = None
else:
allowed = set(self._allow_harmful_protocols)
schemes = self.HARMFUL_PROTOCOLS - allowed
if schemes:
for s in schemes:
if url.lower().startswith(s):
url = '#harmful-link'
break
return url
def text(self, text):
if self._escape:
return escape(text)
return escape_html(text)
def link(self, link, text=None, title=None):
if text is None:
text = link
s = '<a href="' + self._safe_url(link) + '"'
if title:
s += ' title="' + escape_html(title) + '"'
return s + '>' + (text or link) + '</a>'
def image(self, src, alt="", title=None):
src = self._safe_url(src)
alt = escape_html(alt)
s = '<img src="' + src + '" alt="' + alt + '"'
if title:
s += ' title="' + escape_html(title) + '"'
return s + ' />'
def emphasis(self, text):
return '<em>' + text + '</em>'
def strong(self, text):
return '<strong>' + text + '</strong>'
def codespan(self, text):
return '<code>' + escape(text) + '</code>'
def linebreak(self):
return '<br />\n'
def inline_html(self, html):
if self._escape:
return escape(html)
return html
def paragraph(self, text):
return '<p>' + text + '</p>\n'
def heading(self, text, level):
tag = 'h' + str(level)
return '<' + tag + '>' + text + '</' + tag + '>\n'
def newline(self):
return ''
def thematic_break(self):
return '<hr />\n'
def block_text(self, text):
return text
def block_code(self, code, info=None):
html = '<pre><code'
if info is not None:
info = info.strip()
if info:
lang = info.split(None, 1)[0]
lang = escape_html(lang)
html += ' class="language-' + lang + '"'
return html + '>' + escape(code) + '</code></pre>\n'
def block_quote(self, text):
return '<blockquote>\n' + text + '</blockquote>\n'
def block_html(self, html):
if not self._escape:
return html + '\n'
return '<p>' + escape(html) + '</p>\n'
def block_error(self, html):
return '<div class="error">' + html + '</div>\n'
def list(self, text, ordered, level, start=None):
if ordered:
html = '<ol'
if start is not None:
html += ' start="' + str(start) + '"'
return html + '>\n' + text + '</ol>\n'
return '<ul>\n' + text + '</ul>\n'
def list_item(self, text, level):
return '<li>' + text + '</li>\n'
def finalize(self, data):
return ''.join(data)

View File

@ -0,0 +1,121 @@
import re
class Scanner(re.Scanner):
def iter(self, string, state, parse_text):
sc = self.scanner.scanner(string)
pos = 0
for match in iter(sc.search, None):
name, method = self.lexicon[match.lastindex - 1][1]
hole = string[pos:match.start()]
if hole:
yield parse_text(hole, state)
yield method(match, state)
pos = match.end()
hole = string[pos:]
if hole:
yield parse_text(hole, state)
class ScannerParser(object):
scanner_cls = Scanner
RULE_NAMES = tuple()
def __init__(self):
self.rules = list(self.RULE_NAMES)
self.rule_methods = {}
self._cached_sc = {}
def register_rule(self, name, pattern, method):
self.rule_methods[name] = (pattern, lambda m, state: method(self, m, state))
def get_rule_pattern(self, name):
if name not in self.RULE_NAMES:
return self.rule_methods[name][0]
return getattr(self, name.upper())
def get_rule_method(self, name):
if name not in self.RULE_NAMES:
return self.rule_methods[name][1]
return getattr(self, 'parse_' + name)
def parse_text(self, text, state):
raise NotImplementedError
def _scan(self, s, state, rules):
sc = self._create_scanner(rules)
for tok in sc.iter(s, state, self.parse_text):
if isinstance(tok, list):
for t in tok:
yield t
elif tok:
yield tok
def _create_scanner(self, rules):
sc_key = '|'.join(rules)
sc = self._cached_sc.get(sc_key)
if sc:
return sc
lexicon = [
(self.get_rule_pattern(n), (n, self.get_rule_method(n)))
for n in rules
]
sc = self.scanner_cls(lexicon)
self._cached_sc[sc_key] = sc
return sc
class Matcher(object):
PARAGRAPH_END = re.compile(
r'(?:\n{2,})|'
r'(?:\n {0,3}#{1,6})|' # axt heading
r'(?:\n {0,3}(?:`{3,}|~{3,}))|' # fenced code
r'(?:\n {0,3}>)|' # blockquote
r'(?:\n {0,3}(?:[\*\+-]|1[.)]))|' # list
r'(?:\n {0,3}<)' # block html
)
def __init__(self, lexicon):
self.lexicon = lexicon
def search_pos(self, string, pos):
m = self.PARAGRAPH_END.search(string, pos)
if not m:
return None
if set(m.group(0)) == {'\n'}:
return m.end()
return m.start() + 1
def iter(self, string, state, parse_text):
pos = 0
endpos = len(string)
last_end = 0
while 1:
if pos >= endpos:
break
for rule, (name, method) in self.lexicon:
match = rule.match(string, pos)
if match is not None:
start, end = match.span()
if start > last_end:
yield parse_text(string[last_end:start], state)
if name.endswith('_start'):
token = method(match, state, string)
yield token[0]
end = token[1]
else:
yield method(match, state)
last_end = pos = end
break
else:
found = self.search_pos(string, pos)
if found is None:
break
pos = found
if last_end < endpos:
yield parse_text(string[last_end:], state)

View File

@ -0,0 +1,41 @@
try:
from urllib.parse import quote
import html
except ImportError:
from urllib import quote
html = None
PUNCTUATION = r'''\\!"#$%&'()*+,./:;<=>?@\[\]^`{}|_~-'''
ESCAPE_TEXT = r'\\[' + PUNCTUATION + ']'
def escape(s, quote=True):
s = s.replace("&", "&amp;")
s = s.replace("<", "&lt;")
s = s.replace(">", "&gt;")
if quote:
s = s.replace('"', "&quot;")
return s
def escape_url(link):
safe = (
':/?#@' # gen-delims - '[]' (rfc3986)
'!$&()*+,;=' # sub-delims - "'" (rfc3986)
'%' # leave already-encoded octets alone
)
if html is None:
return quote(link.encode('utf-8'), safe=safe)
return html.escape(quote(html.unescape(link), safe=safe))
def escape_html(s):
if html is not None:
return html.escape(html.unescape(s)).replace('&#x27;', "'")
return escape(s)
def unikey(s):
return ' '.join(s.split()).lower()

View File

@ -10,11 +10,11 @@ from copy import deepcopy
from itertools import chain
from typing import List, Optional, Any, Tuple, Dict
import mistune # type: ignore
from . import mistune # type: ignore
# The following try-catch is used to support mistune 0.8.4 and 2.x
try:
from mistune.plugins.table import plugin_table # type: ignore
from mistune.plugins.footnotes import plugin_footnotes # type: ignore
from .mistune.plugins.table import plugin_table # type: ignore
from .mistune.plugins.footnotes import plugin_footnotes # type: ignore
InlineParser = mistune.inline_parser.InlineParser
HTMLRenderer = mistune.renderers.HTMLRenderer
except ModuleNotFoundError:

View File

@ -3,14 +3,15 @@
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
Dependencies:
- name: mistune
python_module: true
debian: python3-mistune
arch: python-mistune
role: mandatory
"""
# No longer a dependency, now included.
# Will be fixed when the code supports mistune 3 ... or never
# Dependencies:
# - name: mistune
# python_module: true
# debian: python3-mistune
# arch: python-mistune
# role: mandatory
# """
import os
from tempfile import NamedTemporaryFile
# Here we import the whole module to make monkeypatch work

View File

@ -1303,40 +1303,6 @@ deps = '{\
"url": null,\
"url_down": null\
},\
"mistune": {\
"arch": "python-mistune",\
"command": "mistune",\
"comments": [],\
"deb_package": "python3-mistune",\
"downloader": null,\
"downloader_str": null,\
"extra_arch": null,\
"extra_deb": null,\
"help_option": "--version",\
"importance": 10000,\
"in_debian": true,\
"is_kicad_plugin": false,\
"is_python": true,\
"module_name": "mistune",\
"name": "mistune",\
"no_cmd_line_version": false,\
"no_cmd_line_version_old": false,\
"output": "populate",\
"plugin_dirs": null,\
"pypi_name": "mistune",\
"role": [\
{\
"desc": null,\
"mandatory": true,\
"max_version": null,\
"output": "populate",\
"version": null\
}\
],\
"tests": [],\
"url": null,\
"url_down": null\
},\
"numpy": {\
"arch": "python-numpy",\
"command": "numpy",\