diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst
index 2dca64d1..9123f947 100644
--- a/docs/source/dependencies.rst
+++ b/docs/source/dependencies.rst
@@ -71,19 +71,15 @@
- Mandatory for `kikit_present`
-`mistune `__ :index:`: ` |image29| |image30|
-
-- Mandatory for `populate`
-
-`QRCodeGen `__ :index:`: ` |image31| |image32| |image33| |Auto-download|
+`QRCodeGen `__ :index:`: ` |image29| |image30| |image31| |Auto-download|
- Mandatory for `qr_lib`
-`Colorama `__ :index:`: ` |image34| |image35| |image36|
+`Colorama `__ :index:`: ` |image32| |image33| |image34|
- Optional to get color messages in a portable way for general use
-`Git `__ :index:`: ` |image37| |image38| |Auto-download|
+`Git `__ :index:`: ` |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 `__ :index:`: ` |image39| |image40| |Auto-download|
+`ImageMagick `__ :index:`: ` |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 `__ :index:`: ` |image41| |image42| |Auto-download|
+`RSVG tools `__ :index:`: ` |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 `__ :index:`: ` |image43| |image44|
+`Bash `__ :index:`: ` |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 `__ :index:`: ` |image45| |image46| |Auto-download|
+`Ghostscript `__ :index:`: ` |image43| |image44| |Auto-download|
- Optional to:
- Create outputs preview for `navigate_results`
- Create PNG, PS and EPS formats for `pcb_print`
-`numpy `__ :index:`: ` |image47| |image48| |Auto-download|
+`numpy `__ :index:`: ` |image45| |image46| |Auto-download|
- Optional to automatically adjust SVG margin for `pcbdraw`
-`Pandoc `__ :index:`: ` |image49| |image50|
+`Pandoc `__ :index:`: ` |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 `__ :index:`: ` |image51| |image52| |Auto-download|
+`RAR `__ :index:`: ` |image49| |image50| |Auto-download|
- Optional to compress in RAR format for `compress`
-`XLSXWriter `__ :index:`: ` v1.1.2 |image53| |image54| |image55| |Auto-download|
+`XLSXWriter `__ :index:`: ` 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
diff --git a/kibot/PcbDraw/mdrenderer.py b/kibot/PcbDraw/mdrenderer.py
index f3d09dfb..662ca72b 100644
--- a/kibot/PcbDraw/mdrenderer.py
+++ b/kibot/PcbDraw/mdrenderer.py
@@ -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
diff --git a/kibot/PcbDraw/mistune/__init__.py b/kibot/PcbDraw/mistune/__init__.py
new file mode 100644
index 00000000..408ed12b
--- /dev/null
+++ b/kibot/PcbDraw/mistune/__init__.py
@@ -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 ``
``.
+ :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'
diff --git a/kibot/PcbDraw/mistune/block_parser.py b/kibot/PcbDraw/mistune/block_parser.py
new file mode 100644
index 00000000..f433fe58
--- /dev/null
+++ b/kibot/PcbDraw/mistune/block_parser.py
@@ -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'[^\n]*\n+|'
+ r'<\?[\s\S]*?\?>[^\n]*\n+|'
+ r'[^\n]*\n+|'
+ r'[^\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
diff --git a/kibot/PcbDraw/mistune/directives/__init__.py b/kibot/PcbDraw/mistune/directives/__init__.py
new file mode 100644
index 00000000..fdc96d78
--- /dev/null
+++ b/kibot/PcbDraw/mistune/directives/__init__.py
@@ -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',
+]
diff --git a/kibot/PcbDraw/mistune/directives/admonition.py b/kibot/PcbDraw/mistune/directives/admonition.py
new file mode 100644
index 00000000..fbb5ceff
--- /dev/null
+++ b/kibot/PcbDraw/mistune/directives/admonition.py
@@ -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 = '\n'
+ if not title:
+ title = name.capitalize()
+ if title:
+ html += '' + title + '
\n'
+ if text:
+ html += text
+ return html + '\n'
+
+
+def render_ast_admonition(children, name, title=""):
+ return {
+ 'type': 'admonition',
+ 'children': children,
+ 'name': name,
+ 'title': title,
+ }
diff --git a/kibot/PcbDraw/mistune/directives/base.py b/kibot/PcbDraw/mistune/directives/base.py
new file mode 100644
index 00000000..54a3d861
--- /dev/null
+++ b/kibot/PcbDraw/mistune/directives/base.py
@@ -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[a-zA-Z0-9_-]+)\:\: *(?P[^\n]*)\n+'
+ r'(?P(?: \1 {0,3}\:[a-zA-Z0-9_-]+\: *[^\n]*\n+)*)'
+ r'(?P(?: \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')
diff --git a/kibot/PcbDraw/mistune/directives/include.py b/kibot/PcbDraw/mistune/directives/include.py
new file mode 100644
index 00000000..3631bba5
--- /dev/null
+++ b/kibot/PcbDraw/mistune/directives/include.py
@@ -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 = '\n'
diff --git a/kibot/PcbDraw/mistune/directives/toc.py b/kibot/PcbDraw/mistune/directives/toc.py
new file mode 100644
index 00000000..5606710b
--- /dev/null
+++ b/kibot/PcbDraw/mistune/directives/toc.py
@@ -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 = '\n'
+ if title:
+ html += '' + title + '
\n'
+
+ return html + render_toc_ul(items) + '\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 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 = '\n'
+ levels = []
+ for k, text, level in toc:
+ item = '{}'.format(k, text)
+ if not levels:
+ s += '- ' + item
+ levels.append(level)
+ elif level == levels[-1]:
+ s += '
\n- ' + item
+ elif level > levels[-1]:
+ s += '\n
\n- ' + item
+ levels.append(level)
+ else:
+ last_level = levels.pop()
+ while levels:
+ last_level = levels.pop()
+ if level == last_level:
+ s += '
\n
\n \n- ' + item
+ levels.append(level)
+ break
+ elif level > last_level:
+ s += '
\n- ' + item
+ levels.append(last_level)
+ levels.append(level)
+ break
+ else:
+ s += '
\n
\n'
+ else:
+ levels.append(level)
+ s += '\n- ' + item
+
+ while len(levels) > 1:
+ s += '
\n
\n'
+ levels.pop()
+
+ return s + '\n\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 ''
diff --git a/kibot/PcbDraw/mistune/inline_parser.py b/kibot/PcbDraw/mistune/inline_parser.py
new file mode 100644
index 00000000..4e6bf743
--- /dev/null
+++ b/kibot/PcbDraw/mistune/inline_parser.py
@@ -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::
+ #:
+ #:
+ AUTO_LINK = (
+ 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'(?:(?:(?|' # open tag
+ r'(?|' # close tag
+ r'(?|->)(?:(?!--)[\s\S])+?(?|' # comment
+ r'(?|'
+ r'(?|' # doctype
+ r'(?' # 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
+ 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(''):
+ 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)
diff --git a/kibot/PcbDraw/mistune/markdown.py b/kibot/PcbDraw/mistune/markdown.py
new file mode 100644
index 00000000..b0a21dc8
--- /dev/null
+++ b/kibot/PcbDraw/mistune/markdown.py
@@ -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
diff --git a/kibot/PcbDraw/mistune/plugins/__init__.py b/kibot/PcbDraw/mistune/plugins/__init__.py
new file mode 100644
index 00000000..5de4c0ac
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/__init__.py
@@ -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",
+]
diff --git a/kibot/PcbDraw/mistune/plugins/abbr.py b/kibot/PcbDraw/mistune/plugins/abbr.py
new file mode 100644
index 00000000..224598a1
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/abbr.py
@@ -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 "{key}".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)
diff --git a/kibot/PcbDraw/mistune/plugins/def_list.py b/kibot/PcbDraw/mistune/plugins/def_list.py
new file mode 100644
index 00000000..9d5e7b14
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/def_list.py
@@ -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 "\n" + text + "
\n"
+
+
+def render_html_def_list_header(text):
+ return "" + text + "\n"
+
+
+def render_html_def_list_item(text):
+ return "" + text + "\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)
diff --git a/kibot/PcbDraw/mistune/plugins/extra.py b/kibot/PcbDraw/mistune/plugins/extra.py
new file mode 100644
index 00000000..34838334
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/extra.py
@@ -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 '' + text + ''
+
+
+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)
diff --git a/kibot/PcbDraw/mistune/plugins/footnotes.py b/kibot/PcbDraw/mistune/plugins/footnotes.py
new file mode 100644
index 00000000..1c3d0530
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/footnotes.py
@@ -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 = ''
+
+
+def render_html_footnotes(text):
+ return (
+ '\n'
+ )
+
+
+def render_html_footnote_item(text, key, index):
+ i = str(index)
+ back = ''
+
+ text = text.rstrip()
+ if text.endswith('
'):
+ text = text[:-4] + back + ''
+ else:
+ text = text + back
+ return '' + text + '\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)
diff --git a/kibot/PcbDraw/mistune/plugins/table.py b/kibot/PcbDraw/mistune/plugins/table.py
new file mode 100644
index 00000000..f8dbce72
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/table.py
@@ -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' *(?\n' + text + '\n'
+
+
+def render_html_table_head(text):
+ return '\n\n' + text + '
\n\n'
+
+
+def render_html_table_body(text):
+ return '\n' + text + '\n'
+
+
+def render_html_table_row(text):
+ return '\n' + text + '
\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)
diff --git a/kibot/PcbDraw/mistune/plugins/task_lists.py b/kibot/PcbDraw/mistune/plugins/task_lists.py
new file mode 100644
index 00000000..3094ea72
--- /dev/null
+++ b/kibot/PcbDraw/mistune/plugins/task_lists.py
@@ -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 = (
+ ''
+ else:
+ checkbox += '/>'
+
+ if text.startswith(''):
+ text = text.replace('
', '
' + checkbox, 1)
+ else:
+ text = checkbox + text
+
+ return '
' + text + '\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
diff --git a/kibot/PcbDraw/mistune/renderers.py b/kibot/PcbDraw/mistune/renderers.py
new file mode 100644
index 00000000..18d3565a
--- /dev/null
+++ b/kibot/PcbDraw/mistune/renderers.py
@@ -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 = '' + (text or link) + ''
+
+ def image(self, src, alt="", title=None):
+ src = self._safe_url(src)
+ alt = escape_html(alt)
+ s = '
'
+
+ def emphasis(self, text):
+ return '' + text + ''
+
+ def strong(self, text):
+ return '' + text + ''
+
+ def codespan(self, text):
+ return '' + escape(text) + ''
+
+ def linebreak(self):
+ return '
\n'
+
+ def inline_html(self, html):
+ if self._escape:
+ return escape(html)
+ return html
+
+ def paragraph(self, text):
+ return '' + text + '
\n'
+
+ def heading(self, text, level):
+ tag = 'h' + str(level)
+ return '<' + tag + '>' + text + '' + tag + '>\n'
+
+ def newline(self):
+ return ''
+
+ def thematic_break(self):
+ return '
\n'
+
+ def block_text(self, text):
+ return text
+
+ def block_code(self, code, info=None):
+ html = '' + escape(code) + '
\n'
+
+ def block_quote(self, text):
+ return '\n' + text + '
\n'
+
+ def block_html(self, html):
+ if not self._escape:
+ return html + '\n'
+ return '' + escape(html) + '
\n'
+
+ def block_error(self, html):
+ return '' + html + '
\n'
+
+ def list(self, text, ordered, level, start=None):
+ if ordered:
+ html = '\n' + text + '
\n'
+ return '\n'
+
+ def list_item(self, text, level):
+ return '' + text + '\n'
+
+ def finalize(self, data):
+ return ''.join(data)
diff --git a/kibot/PcbDraw/mistune/scanner.py b/kibot/PcbDraw/mistune/scanner.py
new file mode 100644
index 00000000..8237b620
--- /dev/null
+++ b/kibot/PcbDraw/mistune/scanner.py
@@ -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)
diff --git a/kibot/PcbDraw/mistune/util.py b/kibot/PcbDraw/mistune/util.py
new file mode 100644
index 00000000..f99fe37e
--- /dev/null
+++ b/kibot/PcbDraw/mistune/util.py
@@ -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()
diff --git a/kibot/PcbDraw/populate.py b/kibot/PcbDraw/populate.py
index 77e29e18..1956e9c6 100644
--- a/kibot/PcbDraw/populate.py
+++ b/kibot/PcbDraw/populate.py
@@ -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:
diff --git a/kibot/out_populate.py b/kibot/out_populate.py
index 62774961..8e263281 100644
--- a/kibot/out_populate.py
+++ b/kibot/out_populate.py
@@ -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
diff --git a/src/kibot-check b/src/kibot-check
index 28cd8d4d..2f52c351 100755
--- a/src/kibot-check
+++ b/src/kibot-check
@@ -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",\