[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:
parent
1cbbed1414
commit
b7add3644d
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
@ -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'
|
||||
|
|
@ -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 ''
|
||||
|
|
@ -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")
|
||||
#: 
|
||||
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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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">↩</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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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("&", "&")
|
||||
s = s.replace("<", "<")
|
||||
s = s.replace(">", ">")
|
||||
if quote:
|
||||
s = s.replace('"', """)
|
||||
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(''', "'")
|
||||
return escape(s)
|
||||
|
||||
|
||||
def unikey(s):
|
||||
return ' '.join(s.split()).lower()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",\
|
||||
|
|
|
|||
Loading…
Reference in New Issue