From 58880ac27ac45d0da48924cbb567b1f32a52d24a Mon Sep 17 00:00:00 2001 From: octospacc Date: Mon, 16 May 2022 20:12:06 +0200 Subject: [PATCH] First working generator --- Assets/Style.css | 44 + Build.py | 162 ++++ Build.sh | 10 + Libs/markdown/__init__.py | 61 ++ Libs/markdown/__main__.py | 151 +++ Libs/markdown/__meta__.py | 49 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 1830 bytes .../__pycache__/__meta__.cpython-310.pyc | Bin 0 -> 1468 bytes .../__pycache__/blockparser.cpython-310.pyc | Bin 0 -> 4973 bytes .../blockprocessors.cpython-310.pyc | Bin 0 -> 17669 bytes .../markdown/__pycache__/core.cpython-310.pyc | Bin 0 -> 12072 bytes .../__pycache__/htmlparser.cpython-310.pyc | Bin 0 -> 9020 bytes .../inlinepatterns.cpython-310.pyc | Bin 0 -> 25217 bytes .../__pycache__/pep562.cpython-310.pyc | Bin 0 -> 8133 bytes .../postprocessors.cpython-310.pyc | Bin 0 -> 4860 bytes .../__pycache__/preprocessors.cpython-310.pyc | Bin 0 -> 3273 bytes .../__pycache__/serializers.cpython-310.pyc | Bin 0 -> 3143 bytes .../treeprocessors.cpython-310.pyc | Bin 0 -> 11875 bytes .../markdown/__pycache__/util.cpython-310.pyc | Bin 0 -> 14328 bytes Libs/markdown/blockparser.py | 125 +++ Libs/markdown/blockprocessors.py | 623 ++++++++++++ Libs/markdown/core.py | 407 ++++++++ Libs/markdown/extensions/__init__.py | 107 +++ .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 4004 bytes Libs/markdown/extensions/abbr.py | 99 ++ Libs/markdown/extensions/admonition.py | 170 ++++ Libs/markdown/extensions/attr_list.py | 166 ++++ Libs/markdown/extensions/codehilite.py | 308 ++++++ Libs/markdown/extensions/def_list.py | 111 +++ Libs/markdown/extensions/extra.py | 58 ++ Libs/markdown/extensions/fenced_code.py | 174 ++++ Libs/markdown/extensions/footnotes.py | 402 ++++++++ Libs/markdown/extensions/legacy_attrs.py | 67 ++ Libs/markdown/extensions/legacy_em.py | 49 + Libs/markdown/extensions/md_in_html.py | 364 +++++++ Libs/markdown/extensions/meta.py | 79 ++ Libs/markdown/extensions/nl2br.py | 33 + Libs/markdown/extensions/sane_lists.py | 54 ++ Libs/markdown/extensions/smarty.py | 263 ++++++ Libs/markdown/extensions/tables.py | 223 +++++ Libs/markdown/extensions/toc.py | 380 ++++++++ Libs/markdown/extensions/wikilinks.py | 87 ++ Libs/markdown/htmlparser.py | 323 +++++++ Libs/markdown/inlinepatterns.py | 892 ++++++++++++++++++ Libs/markdown/pep562.py | 245 +++++ Libs/markdown/postprocessors.py | 134 +++ Libs/markdown/preprocessors.py | 82 ++ Libs/markdown/serializers.py | 189 ++++ Libs/markdown/test_tools.py | 220 +++++ Libs/markdown/treeprocessors.py | 436 +++++++++ Libs/markdown/util.py | 485 ++++++++++ Pages/index.md | 11 + Pages/test/test.md | 1 + Parts/Standard/BottomBox.html | 0 Parts/Standard/LeftBox.html | 0 Parts/Standard/TopBox.html | 0 Templates/Standard.html | 34 + 57 files changed, 7878 insertions(+) create mode 100755 Assets/Style.css create mode 100755 Build.py create mode 100755 Build.sh create mode 100644 Libs/markdown/__init__.py create mode 100644 Libs/markdown/__main__.py create mode 100644 Libs/markdown/__meta__.py create mode 100644 Libs/markdown/__pycache__/__init__.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/__meta__.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/blockparser.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/blockprocessors.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/core.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/htmlparser.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/inlinepatterns.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/pep562.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/postprocessors.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/preprocessors.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/serializers.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/treeprocessors.cpython-310.pyc create mode 100644 Libs/markdown/__pycache__/util.cpython-310.pyc create mode 100644 Libs/markdown/blockparser.py create mode 100644 Libs/markdown/blockprocessors.py create mode 100644 Libs/markdown/core.py create mode 100644 Libs/markdown/extensions/__init__.py create mode 100644 Libs/markdown/extensions/__pycache__/__init__.cpython-310.pyc create mode 100644 Libs/markdown/extensions/abbr.py create mode 100644 Libs/markdown/extensions/admonition.py create mode 100644 Libs/markdown/extensions/attr_list.py create mode 100644 Libs/markdown/extensions/codehilite.py create mode 100644 Libs/markdown/extensions/def_list.py create mode 100644 Libs/markdown/extensions/extra.py create mode 100644 Libs/markdown/extensions/fenced_code.py create mode 100644 Libs/markdown/extensions/footnotes.py create mode 100644 Libs/markdown/extensions/legacy_attrs.py create mode 100644 Libs/markdown/extensions/legacy_em.py create mode 100644 Libs/markdown/extensions/md_in_html.py create mode 100644 Libs/markdown/extensions/meta.py create mode 100644 Libs/markdown/extensions/nl2br.py create mode 100644 Libs/markdown/extensions/sane_lists.py create mode 100644 Libs/markdown/extensions/smarty.py create mode 100644 Libs/markdown/extensions/tables.py create mode 100644 Libs/markdown/extensions/toc.py create mode 100644 Libs/markdown/extensions/wikilinks.py create mode 100644 Libs/markdown/htmlparser.py create mode 100644 Libs/markdown/inlinepatterns.py create mode 100644 Libs/markdown/pep562.py create mode 100644 Libs/markdown/postprocessors.py create mode 100644 Libs/markdown/preprocessors.py create mode 100644 Libs/markdown/serializers.py create mode 100644 Libs/markdown/test_tools.py create mode 100644 Libs/markdown/treeprocessors.py create mode 100644 Libs/markdown/util.py create mode 100755 Pages/index.md create mode 100644 Pages/test/test.md create mode 100644 Parts/Standard/BottomBox.html create mode 100644 Parts/Standard/LeftBox.html create mode 100644 Parts/Standard/TopBox.html create mode 100755 Templates/Standard.html diff --git a/Assets/Style.css b/Assets/Style.css new file mode 100755 index 0000000..8be203f --- /dev/null +++ b/Assets/Style.css @@ -0,0 +1,44 @@ +:root { + --ScreenBorderPadding: 4px; +} + +* { + Box-Sizing: Border-Box; +} + +Body { + Margin: 0; + Padding: var(--ScreenBorderPadding); + Box-Sizing: Border-Box; + Width: 100vw; + Height: 100vh; +} +#Container { + Box-Sizing: Border-Box; +} + +#TopBox { + Border: 2px Solid Purple; + Padding: 8px; +} +#LeftBox { + Float: Left; + //Border: 2px Solid Purple; +} +#RightBox { + Position: Inherit; + Top: 16px; + Right: 16px; + Float: Right; +} +#MainBox { + Text-Align: Center; + Margin: Auto; +} +#BottomBox { + Box-Sizing: Border-Box; + //Border: 2px Solid Purple; + Position: Fixed; + Bottom: var(--ScreenBorderPadding); + Width: 100vw; +} diff --git a/Build.py b/Build.py new file mode 100755 index 0000000..394fb79 --- /dev/null +++ b/Build.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +import os +import shutil +from pathlib import Path +from Libs.markdown import Markdown + +def ReadFile(p): + try: + with open(p, 'r') as f: + return f.read() + except Exception: + print("Error reading file {}".format(p)) + return None + +def WriteFile(p, c): + try: + with open(p, 'w') as f: + f.write(c) + return True + except Exception: + print("Error writing file {}".format(p)) + return False + +def ResetPublic(): + try: + shutil.rmtree('public') + except FileNotFoundError: + pass +# os.mkdir('public') + +def GetTitles(c): + Titles = [] + + return Titles + +def FormatTitles(Titles): + return '' + +""" +def GetAssociations(): + f = ReadFile('Associations.txt') + Templates = f.splitlines() + Uses = {} + for t in Templates: + t = t.split(' ') + Uses.update({t[0][:-1]: t[1:]}) + print(Uses) + return Uses + +def LoadTemplates(): + Templates = {} + for p in Path('Templates').rglob('*.html'): + p = str(p)[len('Templates')+1:] + Templates.update({p: ReadFile('Parts/{}'.format(p))}) + return Templates + + Templates = {} + for t in Associations: + Templates.update({t: ReadFile('Templates/{}'.format(t))}) + return Templates + +def LoadParts(): + Parts = {} + for p in Path('Parts').rglob('*.html'): + p = str(p)[len('Parts')+1:] + Parts.update({p: ReadFile('Parts/{}'.format(p))}) + return Parts +""" + +def LoadFromDir(Dir): + Contents = {} + for File in Path(Dir).rglob('*.html'): + File = str(File)[len(Dir)+1:] + Contents.update({File: ReadFile('{}/{}'.format(Dir, File))}) + return Contents + +def PreProcessor(File): + File = ReadFile(File) + Content, Titles, Meta = '', [], { + 'Template': 'Standard.html', + 'Background': '', + 'Style': ''} + for l in File.splitlines(): + ls = l.lstrip() + if ls.startswith('\%'): + Content += ls[1:] + '\n' + elif ls.startswith('% - '): + Content += ''.format(ls[4:]) + '\n' + elif ls.startswith('% Template: '): + Meta['Template'] = ls[len('% Template: '):] + elif ls.startswith('% Background: '): + Meta['Background'] = ls[len('% Background: '):] + elif ls.startswith('% Style: '): + Meta['Style'] += ls[len('% Style: '):] + ' ' + else: + Content += l + '\n' + Heading = ls.split(' ')[0].count('#') + if Heading > 0: + Titles += [ls] + return Content, Titles, Meta + +""" +def MetaProcessor(Meta): + Meta[] + return Meta +""" + +def PatchHTML(Template, Parts, Content, Titles, Meta): + HTMLTitles = FormatTitles(Titles) + + Template = Template.replace('[HTML:Page:Title]', 'Untitled' if not Titles else Titles[0]) + Template = Template.replace('[HTML:Page:Style]', Meta['Style']) + Template = Template.replace('[HTML:Page:RightBox]', str(Titles)) + Template = Template.replace('[HTML:Page:MainBox]', Content) + + for p in Parts: + Template = Template.replace('[HTML:Part:{}]'.format(p), Parts[p]) + print(Template) + return Template + +def MakeSite(Templates, Parts): + Pages = [] + for File in Path('Pages').rglob('*.md'): + File = str(File)[len('Pages/'):] + Content, Titles, Meta = PreProcessor('Pages/{}'.format(File)) + + Template = Templates[Meta['Template']] + Template = Template.replace( + 'Style.css', + '{}Style.css'.format('../'*File.count('/'))) + + print(Content, Titles, Meta) + WriteFile( + 'public/{}.html'.format(File.rstrip('.md')), + PatchHTML( + Template, + Parts, + Markdown().convert(Content), + Titles, + Meta)) + +def IgnoreFiles(dir, files): + return [f for f in files if os.path.isfile(os.path.join(dir, f))] +def CopyAssets(): + shutil.copytree('Pages', 'public', ignore=IgnoreFiles) +# for File in Path('Assets').rglob('*'): +# File = str(File) +# shutil.copy(File, 'public/{}'.format(File[len('Assets/'):])) + os.system("cp -R Assets/* public/") + +def Main(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + ResetPublic() +# Associations = GetAssociations() + Templates = LoadFromDir('Templates') #LoadTemplates(Associations) + Parts = LoadFromDir('Parts') #LoadParts() + print(Templates, Parts) + CopyAssets() + MakeSite(Templates, Parts) + +if __name__ == '__main__': + Main() diff --git a/Build.sh b/Build.sh new file mode 100755 index 0000000..e41ae43 --- /dev/null +++ b/Build.sh @@ -0,0 +1,10 @@ +#!/bin/sh +cd "$( dirname "$( realpath "$0" )" )" + +mkdir -p public + +cp -R Pages/* public/ +find public -type f -name "*.html" -exec sh -c 'cat Templates/Standard.html > "$1"' _ {} \; +find public -type f -name "*.html" -print0 | xargs -0 sed -i -e 's/\[HTML\:PageTitle\]/ERRE/g' + +cp Style.css public/ diff --git a/Libs/markdown/__init__.py b/Libs/markdown/__init__.py new file mode 100644 index 0000000..e05af10 --- /dev/null +++ b/Libs/markdown/__init__.py @@ -0,0 +1,61 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import sys + +# TODO: Remove this check at some point in the future. +# (also remove flake8's 'ignore E402' comments below) +if sys.version_info[0] < 3: # pragma: no cover + raise ImportError('A recent version of Python 3 is required.') + +from .core import Markdown, markdown, markdownFromFile # noqa: E402 +from .util import PY37 # noqa: E402 +from .pep562 import Pep562 # noqa: E402 +from .__meta__ import __version__, __version_info__ # noqa: E402 +import warnings # noqa: E402 + +# For backward compatibility as some extensions expect it... +from .extensions import Extension # noqa + +__all__ = ['Markdown', 'markdown', 'markdownFromFile'] + +__deprecated__ = { + "version": ("__version__", __version__), + "version_info": ("__version_info__", __version_info__) +} + + +def __getattr__(name): + """Get attribute.""" + + deprecated = __deprecated__.get(name) + if deprecated: + warnings.warn( + "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]), + category=DeprecationWarning, + stacklevel=(3 if PY37 else 4) + ) + return deprecated[1] + raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name)) + + +if not PY37: + Pep562(__name__) diff --git a/Libs/markdown/__main__.py b/Libs/markdown/__main__.py new file mode 100644 index 0000000..0184008 --- /dev/null +++ b/Libs/markdown/__main__.py @@ -0,0 +1,151 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import sys +import optparse +import codecs +import warnings +import markdown +try: + # We use `unsafe_load` because users may need to pass in actual Python + # objects. As this is only available from the CLI, the user has much + # worse problems if an attacker can use this as an attach vector. + from yaml import unsafe_load as yaml_load +except ImportError: # pragma: no cover + try: + # Fall back to PyYAML <5.1 + from yaml import load as yaml_load + except ImportError: + # Fall back to JSON + from json import load as yaml_load + +import logging +from logging import DEBUG, WARNING, CRITICAL + +logger = logging.getLogger('MARKDOWN') + + +def parse_options(args=None, values=None): + """ + Define and parse `optparse` options for command-line usage. + """ + usage = """%prog [options] [INPUTFILE] + (STDIN is assumed if no INPUTFILE is given)""" + desc = "A Python implementation of John Gruber's Markdown. " \ + "https://Python-Markdown.github.io/" + ver = "%%prog %s" % markdown.__version__ + + parser = optparse.OptionParser(usage=usage, description=desc, version=ver) + parser.add_option("-f", "--file", dest="filename", default=None, + help="Write output to OUTPUT_FILE. Defaults to STDOUT.", + metavar="OUTPUT_FILE") + parser.add_option("-e", "--encoding", dest="encoding", + help="Encoding for input and output files.",) + parser.add_option("-o", "--output_format", dest="output_format", + default='xhtml', metavar="OUTPUT_FORMAT", + help="Use output format 'xhtml' (default) or 'html'.") + parser.add_option("-n", "--no_lazy_ol", dest="lazy_ol", + action='store_false', default=True, + help="Observe number of first item of ordered lists.") + parser.add_option("-x", "--extension", action="append", dest="extensions", + help="Load extension EXTENSION.", metavar="EXTENSION") + parser.add_option("-c", "--extension_configs", + dest="configfile", default=None, + help="Read extension configurations from CONFIG_FILE. " + "CONFIG_FILE must be of JSON or YAML format. YAML " + "format requires that a python YAML library be " + "installed. The parsed JSON or YAML must result in a " + "python dictionary which would be accepted by the " + "'extension_configs' keyword on the markdown.Markdown " + "class. The extensions must also be loaded with the " + "`--extension` option.", + metavar="CONFIG_FILE") + parser.add_option("-q", "--quiet", default=CRITICAL, + action="store_const", const=CRITICAL+10, dest="verbose", + help="Suppress all warnings.") + parser.add_option("-v", "--verbose", + action="store_const", const=WARNING, dest="verbose", + help="Print all warnings.") + parser.add_option("--noisy", + action="store_const", const=DEBUG, dest="verbose", + help="Print debug messages.") + + (options, args) = parser.parse_args(args, values) + + if len(args) == 0: + input_file = None + else: + input_file = args[0] + + if not options.extensions: + options.extensions = [] + + extension_configs = {} + if options.configfile: + with codecs.open( + options.configfile, mode="r", encoding=options.encoding + ) as fp: + try: + extension_configs = yaml_load(fp) + except Exception as e: + message = "Failed parsing extension config file: %s" % \ + options.configfile + e.args = (message,) + e.args[1:] + raise + + opts = { + 'input': input_file, + 'output': options.filename, + 'extensions': options.extensions, + 'extension_configs': extension_configs, + 'encoding': options.encoding, + 'output_format': options.output_format, + 'lazy_ol': options.lazy_ol + } + + return opts, options.verbose + + +def run(): # pragma: no cover + """Run Markdown from the command line.""" + + # Parse options and adjust logging level if necessary + options, logging_level = parse_options() + if not options: + sys.exit(2) + logger.setLevel(logging_level) + console_handler = logging.StreamHandler() + logger.addHandler(console_handler) + if logging_level <= WARNING: + # Ensure deprecation warnings get displayed + warnings.filterwarnings('default') + logging.captureWarnings(True) + warn_logger = logging.getLogger('py.warnings') + warn_logger.addHandler(console_handler) + + # Run + markdown.markdownFromFile(**options) + + +if __name__ == '__main__': # pragma: no cover + # Support running module as a commandline command. + # `python -m markdown [options] [args]`. + run() diff --git a/Libs/markdown/__meta__.py b/Libs/markdown/__meta__.py new file mode 100644 index 0000000..84884b6 --- /dev/null +++ b/Libs/markdown/__meta__.py @@ -0,0 +1,49 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +# __version_info__ format: +# (major, minor, patch, dev/alpha/beta/rc/final, #) +# (1, 1, 2, 'dev', 0) => "1.1.2.dev0" +# (1, 1, 2, 'alpha', 1) => "1.1.2a1" +# (1, 2, 0, 'beta', 2) => "1.2b2" +# (1, 2, 0, 'rc', 4) => "1.2rc4" +# (1, 2, 0, 'final', 0) => "1.2" +__version_info__ = (3, 3, 7, 'final', 0) + + +def _get_version(version_info): + " Returns a PEP 440-compliant version number from version_info. " + assert len(version_info) == 5 + assert version_info[3] in ('dev', 'alpha', 'beta', 'rc', 'final') + + parts = 2 if version_info[2] == 0 else 3 + v = '.'.join(map(str, version_info[:parts])) + + if version_info[3] == 'dev': + v += '.dev' + str(version_info[4]) + elif version_info[3] != 'final': + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'} + v += mapping[version_info[3]] + str(version_info[4]) + + return v + + +__version__ = _get_version(__version_info__) diff --git a/Libs/markdown/__pycache__/__init__.cpython-310.pyc b/Libs/markdown/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3598f4c8f41e8079da844adafa0d26df748db639 GIT binary patch literal 1830 zcmZuyTW{Mo6c%M!mgP&6v>n!601nvD8fc^>U7Mj8hIL8WrAUfk?od=O0&UW^8cEbh zI-a`pW!>xc3--|NY5Plh-BbR;fL#t{t4_NL;4v@feCInC(!A4g4fuYs)L_*yj6e0_ z{8xj;moVh-AgB=-6j3uq0b=NhNzoO9){YIa%4*blj($Y6PVLxA>OnoR13PI14P?Mx zBX*Kz&@AUp>?WQSCZ9W6&YiL+P|S-jkZ{4WYRY4 zg6#Zi1{Z0UUiirfF0rQkhPBw`=LitYemQz8q%aM`J<8RDr}&VFW6Ec#>)yeOttiQ2 zmatS26@dmH;ro1&;(KB`VB)HLW#YN+E+0-`Y2L;Yr80Tj_p=gvJy~FT<48@W125vf zdoNO7PX|lY#nF%_{@nd~#mKMBec*buzck76Eb_P*`!#N{DYM{qc5#fjA zc%oR&PF_O+2W%Ee$`efpkang*K<45cCreDgctrA;r1&68Acx~QhR)Qv@Mon;@4CC2 zL`vkiKOw_OS~+|}8B}s|bGwI0O7XrVWQY%^lD!ppM20M8Qo^|aitY~2auJOu3U6&} zY_D%^Y<`9xPFPj`Dx3Ido?d+I#KW83cIhf63P4_m`1HC4x>?Fw|H~E_Z+Khlo8GO# zn|5z2kpdzd*hMLc@d*@hBvYWTIdS=lI!v)_kELN6_C>`-I1l8u=_X>i5fJ76V3H`W$U1MQY zv@3IMG1Ppj5ALgB!`Oi#?}Jdr3v_C{M9xYXo5mBJF`GQAdXfYbCDOWRAhJ@vlAAJvJM|5oKE8f!h}ei@}P_c{9%aq!04|{f5H>y^Py7yE<5ovE>*cWhz4@`5QSlsMk)-wEZ5}* zemZTi1`N3d!mU}zLS1Ac7ukP1^%W#OfbHHpMFWE4LmidOrRczB(b`WkE>uY~u!dYP zp))D!TIg~Y)>K7t8CGCtWuFQ7A2I@nDFNSMxNIbGQ5(3dYB4EVdIQ{&I0nU4Se0DF zyLzUhJ}2JSy3XhGDt)0VD?bKd8P4Ud<)BMu8@162a!|YGn0C!Z_CJnob=%1L_dk`! B8@>Pl literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/__meta__.cpython-310.pyc b/Libs/markdown/__pycache__/__meta__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eba0c09acfb7ee19212885b84368cb7b3700f4d GIT binary patch literal 1468 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 Kqc4uX{J#Nz^ImEI literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/core.cpython-310.pyc b/Libs/markdown/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1c9c2262df86f9557ee498e06108c0cb9aef199 GIT binary patch literal 12072 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 ND_UOvlR0xl{Xb?hrGo$f literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/inlinepatterns.cpython-310.pyc b/Libs/markdown/__pycache__/inlinepatterns.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36311d0c82b26831c7b40ceaf9aebe41ab4ab9a5 GIT binary patch literal 25217 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 literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/pep562.cpython-310.pyc b/Libs/markdown/__pycache__/pep562.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78221da6d28c8ec84916fed2264c7c37b1cd678f GIT binary patch literal 8133 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 Cd5Ts5 literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/serializers.cpython-310.pyc b/Libs/markdown/__pycache__/serializers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e20d0384f5c383aa578a729394cff64990316519 GIT binary patch literal 3143 zcmb_eO>i7X74Gio`P-FN{|*TOuVf@VIQj|UCqaswl%pyVY~f&6QeNzu8f~}am1k$x zJ-wE+o;?tSTj0c%D(M6|z%i9W&TyhQaO*2o6uFX{F6O;nOL6R|a)90HH?Lp!>wfR+ z*Y9;cJX|60`z_ZSA3aaVKe6-VFM!S)(273-5k%0Gv}uD<krh=6Wy9@nRd&{`3tkc!TZ-FU7?i!x9 zvU`TNrB007Nnmm>n!9*;`r_rE+`jP3rTN$HTq)p8&Ek{j??mhk? z??Jl)tvCln!zmrml4w?veY))&vVHmijo5ncd^5?iT-OV!T1ndK$@;@-)vsUFdmUi= zQN&Gn6Zkcya%H^DT(uKzI1Za2jx+Emj*lovYK3J@eda7=54u{H?>}TOFmfa zBF z9D>k#EP&}v3-I>=QB_S2r~tnYS)T~U%BD(kz=R{XmAO`S-pX9zp{ykDlN*E+?P$Kq z_Ff*uqlTIL5a+ib(r=I*68Y=Lbne)lOWK{~W4dz8=TZ&KDll7m`N%WgFV^BWuU$7z z(cLh6GtgfqX zgH7r@k~)%UBr`}Z06C^J$KKriZmyRNt@PZv3okD%tMB0OS7B8Rw0W3vHfXyr+Pn1s2JNX3TRsDz@M{24-vjH^B_wB}GVx_pY#M8VM)3lWXQ4Tb zr3DynIzUc-SQk$7%D*AYlysZ)9iX(&OGmRVQ4=NP8ts&PpPsX~CHn`Aa2Tn>2>a5= zUrI+17LtidRts-fN#7!Og1(D4u5?Sb?@)4oA^Wo+eNTI^p1;pjqja_@)Ewv6 z?0F*iJ*M8X5?B)@P-9#HneI00dwnP4IusseJ+Q(ro#(7=B|WGdlD=R1>i4=@`uh}l z*n0`If_?~l1$M99G5{X~=!drdt1^Us2&)d2A!vnVP=;W^?@A^D`z0bQ!vO;KQ`oa| zFpP6B^MWsF>8U?}w5n?*ST&djxrwygu`=Khvw$ciU-JygoBq%ox`eRhY84YSTc(j8`;OtD}uu=)F|-*veuwZX8%t zVE`+PP@DG@LwiO}3y8FlZYo${3!P+;dR?617M--EP365L?LIDSZcEE{0XyYNOQA6G z6&6l~slm8uz9W^X{PgCvtMS^k>o;#fy-QkYR59+30#(B$JQ)TWKSDJ$%uv}F#sXP% zAnV6n)l$n=e9zDag9MM&(q?(}W8gHj;x-Tu@_9hVA2)7uJZ5+9ROmQ+kp>L2KDU{_ zLOoUiNx+~2uyMwr5=>H*pkBqMy&7<%u;w_{PTWa-A4n77I{9&6uw8*xyaWWogLG-( zy+b~rTcpoQhF4wF5=v-E@j}9r0;Go&!h@|F0xu{p>2zc!>=uS^=dBE$4_xKBw$K=k z6->PWt3Tzwsm2yZdtiS;6EM{bfs3AXLcIgsXEOAOy`N9Qs3u?=T(^chdjK3u{S_W?y9}f@SdrujqFx7nm?Q;ZblAgK0`WcMlUa!DcT}5&Y z$M@^^tm)S4GfnJKsd3t*6x3r{>gZ9os=8*Ls$FH_R^ah#-S94mYd z7{8gfJFQe2`nH-yfm29rLVUN{XWQUyfiD9a&V%T-VSun&Hx=6TgsrCrn_#7#i*71c n@v;|lK!(0M36Tf!#cGT}B;ql#M*$DKU_6)z#wxD`)gk_0ag@c( literal 0 HcmV?d00001 diff --git a/Libs/markdown/__pycache__/treeprocessors.cpython-310.pyc b/Libs/markdown/__pycache__/treeprocessors.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3337ab021a1d4d225de188e41a6f0c8a00e36227 GIT binary patch literal 11875 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

k-W7q7EU6U=2g^>}^qbysk$=22v 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 W3z@=j;jx0Qe9C;uDB$Ae-Twy9D&w>O literal 0 HcmV?d00001 diff --git a/Libs/markdown/blockparser.py b/Libs/markdown/blockparser.py new file mode 100644 index 0000000..39219fd --- /dev/null +++ b/Libs/markdown/blockparser.py @@ -0,0 +1,125 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import xml.etree.ElementTree as etree +from . import util + + +class State(list): + """ Track the current and nested state of the parser. + + This utility class is used to track the state of the BlockParser and + support multiple levels if nesting. It's just a simple API wrapped around + a list. Each time a state is set, that state is appended to the end of the + list. Each time a state is reset, that state is removed from the end of + the list. + + Therefore, each time a state is set for a nested block, that state must be + reset when we back out of that level of nesting or the state could be + corrupted. + + While all the methods of a list object are available, only the three + defined below need be used. + + """ + + def set(self, state): + """ Set a new state. """ + self.append(state) + + def reset(self): + """ Step back one step in nested state. """ + self.pop() + + def isstate(self, state): + """ Test that top (current) level is of given state. """ + if len(self): + return self[-1] == state + else: + return False + + +class BlockParser: + """ Parse Markdown blocks into an ElementTree object. + + A wrapper class that stitches the various BlockProcessors together, + looping through them and creating an ElementTree object. + """ + + def __init__(self, md): + self.blockprocessors = util.Registry() + self.state = State() + self.md = md + + @property + @util.deprecated("Use 'md' instead.") + def markdown(self): + # TODO: remove this later + return self.md + + def parseDocument(self, lines): + """ Parse a markdown document into an ElementTree. + + Given a list of lines, an ElementTree object (not just a parent + Element) is created and the root element is passed to the parser + as the parent. The ElementTree object is returned. + + This should only be called on an entire document, not pieces. + + """ + # Create a ElementTree from the lines + self.root = etree.Element(self.md.doc_tag) + self.parseChunk(self.root, '\n'.join(lines)) + return etree.ElementTree(self.root) + + def parseChunk(self, parent, text): + """ Parse a chunk of markdown text and attach to given etree node. + + While the ``text`` argument is generally assumed to contain multiple + blocks which will be split on blank lines, it could contain only one + block. Generally, this method would be called by extensions when + block parsing is required. + + The ``parent`` etree Element passed in is altered in place. + Nothing is returned. + + """ + self.parseBlocks(parent, text.split('\n\n')) + + def parseBlocks(self, parent, blocks): + """ Process blocks of markdown text and attach to given etree node. + + Given a list of ``blocks``, each blockprocessor is stepped through + until there are no blocks left. While an extension could potentially + call this method directly, it's generally expected to be used + internally. + + This is a public method as an extension may need to add/alter + additional BlockProcessors which call this method to recursively + parse a nested block. + + """ + while blocks: + for processor in self.blockprocessors: + if processor.test(parent, blocks[0]): + if processor.run(parent, blocks) is not False: + # run returns True or None + break diff --git a/Libs/markdown/blockprocessors.py b/Libs/markdown/blockprocessors.py new file mode 100644 index 0000000..3d0ff86 --- /dev/null +++ b/Libs/markdown/blockprocessors.py @@ -0,0 +1,623 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). + +CORE MARKDOWN BLOCKPARSER +=========================================================================== + +This parser handles basic parsing of Markdown blocks. It doesn't concern +itself with inline elements such as **bold** or *italics*, but rather just +catches blocks, lists, quotes, etc. + +The BlockParser is made up of a bunch of BlockProcessors, each handling a +different type of block. Extensions may add/replace/remove BlockProcessors +as they need to alter how markdown blocks are parsed. +""" + +import logging +import re +import xml.etree.ElementTree as etree +from . import util +from .blockparser import BlockParser + +logger = logging.getLogger('MARKDOWN') + + +def build_block_parser(md, **kwargs): + """ Build the default block parser used by Markdown. """ + parser = BlockParser(md) + parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100) + parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90) + parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80) + parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70) + parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60) + parser.blockprocessors.register(HRProcessor(parser), 'hr', 50) + parser.blockprocessors.register(OListProcessor(parser), 'olist', 40) + parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30) + parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20) + parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15) + parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10) + return parser + + +class BlockProcessor: + """ Base class for block processors. + + Each subclass will provide the methods below to work with the source and + tree. Each processor will need to define it's own ``test`` and ``run`` + methods. The ``test`` method should return True or False, to indicate + whether the current block should be processed by this processor. If the + test passes, the parser will call the processors ``run`` method. + + """ + + def __init__(self, parser): + self.parser = parser + self.tab_length = parser.md.tab_length + + def lastChild(self, parent): + """ Return the last child of an etree element. """ + if len(parent): + return parent[-1] + else: + return None + + def detab(self, text, length=None): + """ Remove a tab from the front of each line of the given text. """ + if length is None: + length = self.tab_length + newtext = [] + lines = text.split('\n') + for line in lines: + if line.startswith(' ' * length): + newtext.append(line[length:]) + elif not line.strip(): + newtext.append('') + else: + break + return '\n'.join(newtext), '\n'.join(lines[len(newtext):]) + + def looseDetab(self, text, level=1): + """ Remove a tab from front of lines but allowing dedented lines. """ + lines = text.split('\n') + for i in range(len(lines)): + if lines[i].startswith(' '*self.tab_length*level): + lines[i] = lines[i][self.tab_length*level:] + return '\n'.join(lines) + + def test(self, parent, block): + """ Test for block type. Must be overridden by subclasses. + + As the parser loops through processors, it will call the ``test`` + method on each to determine if the given block of text is of that + type. This method must return a boolean ``True`` or ``False``. The + actual method of testing is left to the needs of that particular + block type. It could be as simple as ``block.startswith(some_string)`` + or a complex regular expression. As the block type may be different + depending on the parent of the block (i.e. inside a list), the parent + etree element is also provided and may be used as part of the test. + + Keywords: + + * ``parent``: A etree element which will be the parent of the block. + * ``block``: A block of text from the source which has been split at + blank lines. + """ + pass # pragma: no cover + + def run(self, parent, blocks): + """ Run processor. Must be overridden by subclasses. + + When the parser determines the appropriate type of a block, the parser + will call the corresponding processor's ``run`` method. This method + should parse the individual lines of the block and append them to + the etree. + + Note that both the ``parent`` and ``etree`` keywords are pointers + to instances of the objects which should be edited in place. Each + processor must make changes to the existing objects as there is no + mechanism to return new/different objects to replace them. + + This means that this method should be adding SubElements or adding text + to the parent, and should remove (``pop``) or add (``insert``) items to + the list of blocks. + + Keywords: + + * ``parent``: A etree element which is the parent of the current block. + * ``blocks``: A list of all remaining blocks of the document. + """ + pass # pragma: no cover + + +class ListIndentProcessor(BlockProcessor): + """ Process children of list items. + + Example: + * a list item + process this part + + or this part + + """ + + ITEM_TYPES = ['li'] + LIST_TYPES = ['ul', 'ol'] + + def __init__(self, *args): + super().__init__(*args) + self.INDENT_RE = re.compile(r'^(([ ]{%s})+)' % self.tab_length) + + def test(self, parent, block): + return block.startswith(' '*self.tab_length) and \ + not self.parser.state.isstate('detabbed') and \ + (parent.tag in self.ITEM_TYPES or + (len(parent) and parent[-1] is not None and + (parent[-1].tag in self.LIST_TYPES))) + + def run(self, parent, blocks): + block = blocks.pop(0) + level, sibling = self.get_level(parent, block) + block = self.looseDetab(block, level) + + self.parser.state.set('detabbed') + if parent.tag in self.ITEM_TYPES: + # It's possible that this parent has a 'ul' or 'ol' child list + # with a member. If that is the case, then that should be the + # parent. This is intended to catch the edge case of an indented + # list whose first member was parsed previous to this point + # see OListProcessor + if len(parent) and parent[-1].tag in self.LIST_TYPES: + self.parser.parseBlocks(parent[-1], [block]) + else: + # The parent is already a li. Just parse the child block. + self.parser.parseBlocks(parent, [block]) + elif sibling.tag in self.ITEM_TYPES: + # The sibling is a li. Use it as parent. + self.parser.parseBlocks(sibling, [block]) + elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES: + # The parent is a list (``ol`` or ``ul``) which has children. + # Assume the last child li is the parent of this block. + if sibling[-1].text: + # If the parent li has text, that text needs to be moved to a p + # The p must be 'inserted' at beginning of list in the event + # that other children already exist i.e.; a nested sublist. + p = etree.Element('p') + p.text = sibling[-1].text + sibling[-1].text = '' + sibling[-1].insert(0, p) + self.parser.parseChunk(sibling[-1], block) + else: + self.create_item(sibling, block) + self.parser.state.reset() + + def create_item(self, parent, block): + """ Create a new li and parse the block with it as the parent. """ + li = etree.SubElement(parent, 'li') + self.parser.parseBlocks(li, [block]) + + def get_level(self, parent, block): + """ Get level of indent based on list level. """ + # Get indent level + m = self.INDENT_RE.match(block) + if m: + indent_level = len(m.group(1))/self.tab_length + else: + indent_level = 0 + if self.parser.state.isstate('list'): + # We're in a tightlist - so we already are at correct parent. + level = 1 + else: + # We're in a looselist - so we need to find parent. + level = 0 + # Step through children of tree to find matching indent level. + while indent_level > level: + child = self.lastChild(parent) + if (child is not None and + (child.tag in self.LIST_TYPES or child.tag in self.ITEM_TYPES)): + if child.tag in self.LIST_TYPES: + level += 1 + parent = child + else: + # No more child levels. If we're short of indent_level, + # we have a code block. So we stop here. + break + return level, parent + + +class CodeBlockProcessor(BlockProcessor): + """ Process code blocks. """ + + def test(self, parent, block): + return block.startswith(' '*self.tab_length) + + def run(self, parent, blocks): + sibling = self.lastChild(parent) + block = blocks.pop(0) + theRest = '' + if (sibling is not None and sibling.tag == "pre" and + len(sibling) and sibling[0].tag == "code"): + # The previous block was a code block. As blank lines do not start + # new code blocks, append this block to the previous, adding back + # linebreaks removed from the split into a list. + code = sibling[0] + block, theRest = self.detab(block) + code.text = util.AtomicString( + '{}\n{}\n'.format(code.text, util.code_escape(block.rstrip())) + ) + else: + # This is a new codeblock. Create the elements and insert text. + pre = etree.SubElement(parent, 'pre') + code = etree.SubElement(pre, 'code') + block, theRest = self.detab(block) + code.text = util.AtomicString('%s\n' % util.code_escape(block.rstrip())) + if theRest: + # This block contained unindented line(s) after the first indented + # line. Insert these lines as the first block of the master blocks + # list for future processing. + blocks.insert(0, theRest) + + +class BlockQuoteProcessor(BlockProcessor): + + RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)') + + def test(self, parent, block): + return bool(self.RE.search(block)) and not util.nearing_recursion_limit() + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + if m: + before = block[:m.start()] # Lines before blockquote + # Pass lines before blockquote in recursively for parsing first. + self.parser.parseBlocks(parent, [before]) + # Remove ``> `` from beginning of each line. + block = '\n'.join( + [self.clean(line) for line in block[m.start():].split('\n')] + ) + sibling = self.lastChild(parent) + if sibling is not None and sibling.tag == "blockquote": + # Previous block was a blockquote so set that as this blocks parent + quote = sibling + else: + # This is a new blockquote. Create a new parent element. + quote = etree.SubElement(parent, 'blockquote') + # Recursively parse block with blockquote as parent. + # change parser state so blockquotes embedded in lists use p tags + self.parser.state.set('blockquote') + self.parser.parseChunk(quote, block) + self.parser.state.reset() + + def clean(self, line): + """ Remove ``>`` from beginning of a line. """ + m = self.RE.match(line) + if line.strip() == ">": + return "" + elif m: + return m.group(2) + else: + return line + + +class OListProcessor(BlockProcessor): + """ Process ordered list blocks. """ + + TAG = 'ol' + # The integer (python string) with which the lists starts (default=1) + # Eg: If list is initialized as) + # 3. Item + # The ol tag will get starts="3" attribute + STARTSWITH = '1' + # Lazy ol - ignore startswith + LAZY_OL = True + # List of allowed sibling tags. + SIBLING_TAGS = ['ol', 'ul'] + + def __init__(self, parser): + super().__init__(parser) + # Detect an item (``1. item``). ``group(1)`` contains contents of item. + self.RE = re.compile(r'^[ ]{0,%d}\d+\.[ ]+(.*)' % (self.tab_length - 1)) + # Detect items on secondary lines. they can be of either list type. + self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.)|[*+-])[ ]+(.*)' % + (self.tab_length - 1)) + # Detect indented (nested) items of either type + self.INDENT_RE = re.compile(r'^[ ]{%d,%d}((\d+\.)|[*+-])[ ]+.*' % + (self.tab_length, self.tab_length * 2 - 1)) + + def test(self, parent, block): + return bool(self.RE.match(block)) + + def run(self, parent, blocks): + # Check fr multiple items in one block. + items = self.get_items(blocks.pop(0)) + sibling = self.lastChild(parent) + + if sibling is not None and sibling.tag in self.SIBLING_TAGS: + # Previous block was a list item, so set that as parent + lst = sibling + # make sure previous item is in a p- if the item has text, + # then it isn't in a p + if lst[-1].text: + # since it's possible there are other children for this + # sibling, we can't just SubElement the p, we need to + # insert it as the first item. + p = etree.Element('p') + p.text = lst[-1].text + lst[-1].text = '' + lst[-1].insert(0, p) + # if the last item has a tail, then the tail needs to be put in a p + # likely only when a header is not followed by a blank line + lch = self.lastChild(lst[-1]) + if lch is not None and lch.tail: + p = etree.SubElement(lst[-1], 'p') + p.text = lch.tail.lstrip() + lch.tail = '' + + # parse first block differently as it gets wrapped in a p. + li = etree.SubElement(lst, 'li') + self.parser.state.set('looselist') + firstitem = items.pop(0) + self.parser.parseBlocks(li, [firstitem]) + self.parser.state.reset() + elif parent.tag in ['ol', 'ul']: + # this catches the edge case of a multi-item indented list whose + # first item is in a blank parent-list item: + # * * subitem1 + # * subitem2 + # see also ListIndentProcessor + lst = parent + else: + # This is a new list so create parent with appropriate tag. + lst = etree.SubElement(parent, self.TAG) + # Check if a custom start integer is set + if not self.LAZY_OL and self.STARTSWITH != '1': + lst.attrib['start'] = self.STARTSWITH + + self.parser.state.set('list') + # Loop through items in block, recursively parsing each with the + # appropriate parent. + for item in items: + if item.startswith(' '*self.tab_length): + # Item is indented. Parse with last item as parent + self.parser.parseBlocks(lst[-1], [item]) + else: + # New item. Create li and parse with it as parent + li = etree.SubElement(lst, 'li') + self.parser.parseBlocks(li, [item]) + self.parser.state.reset() + + def get_items(self, block): + """ Break a block into list items. """ + items = [] + for line in block.split('\n'): + m = self.CHILD_RE.match(line) + if m: + # This is a new list item + # Check first item for the start index + if not items and self.TAG == 'ol': + # Detect the integer value of first list item + INTEGER_RE = re.compile(r'(\d+)') + self.STARTSWITH = INTEGER_RE.match(m.group(1)).group() + # Append to the list + items.append(m.group(3)) + elif self.INDENT_RE.match(line): + # This is an indented (possibly nested) item. + if items[-1].startswith(' '*self.tab_length): + # Previous item was indented. Append to that item. + items[-1] = '{}\n{}'.format(items[-1], line) + else: + items.append(line) + else: + # This is another line of previous item. Append to that item. + items[-1] = '{}\n{}'.format(items[-1], line) + return items + + +class UListProcessor(OListProcessor): + """ Process unordered list blocks. """ + + TAG = 'ul' + + def __init__(self, parser): + super().__init__(parser) + # Detect an item (``1. item``). ``group(1)`` contains contents of item. + self.RE = re.compile(r'^[ ]{0,%d}[*+-][ ]+(.*)' % (self.tab_length - 1)) + + +class HashHeaderProcessor(BlockProcessor): + """ Process Hash Headers. """ + + # Detect a header at start of any line in block + RE = re.compile(r'(?:^|\n)(?P#{1,6})(?P

(?:\\.|[^\\])*?)#*(?:\n|$)') + + def test(self, parent, block): + return bool(self.RE.search(block)) + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + if m: + before = block[:m.start()] # All lines before header + after = block[m.end():] # All lines after header + if before: + # As the header was not the first line of the block and the + # lines before the header must be parsed first, + # recursively parse this lines as a block. + self.parser.parseBlocks(parent, [before]) + # Create header using named groups from RE + h = etree.SubElement(parent, 'h%d' % len(m.group('level'))) + h.text = m.group('header').strip() + if after: + # Insert remaining lines as first block for future parsing. + blocks.insert(0, after) + else: # pragma: no cover + # This should never happen, but just in case... + logger.warn("We've got a problem header: %r" % block) + + +class SetextHeaderProcessor(BlockProcessor): + """ Process Setext-style Headers. """ + + # Detect Setext-style header. Must be first 2 lines of block. + RE = re.compile(r'^.*?\n[=-]+[ ]*(\n|$)', re.MULTILINE) + + def test(self, parent, block): + return bool(self.RE.match(block)) + + def run(self, parent, blocks): + lines = blocks.pop(0).split('\n') + # Determine level. ``=`` is 1 and ``-`` is 2. + if lines[1].startswith('='): + level = 1 + else: + level = 2 + h = etree.SubElement(parent, 'h%d' % level) + h.text = lines[0].strip() + if len(lines) > 2: + # Block contains additional lines. Add to master blocks for later. + blocks.insert(0, '\n'.join(lines[2:])) + + +class HRProcessor(BlockProcessor): + """ Process Horizontal Rules. """ + + # Python's re module doesn't officially support atomic grouping. However you can fake it. + # See https://stackoverflow.com/a/13577411/866026 + RE = r'^[ ]{0,3}(?=(?P(-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,}))(?P=atomicgroup)[ ]*$' + # Detect hr on any line of a block. + SEARCH_RE = re.compile(RE, re.MULTILINE) + + def test(self, parent, block): + m = self.SEARCH_RE.search(block) + if m: + # Save match object on class instance so we can use it later. + self.match = m + return True + return False + + def run(self, parent, blocks): + block = blocks.pop(0) + match = self.match + # Check for lines in block before hr. + prelines = block[:match.start()].rstrip('\n') + if prelines: + # Recursively parse lines before hr so they get parsed first. + self.parser.parseBlocks(parent, [prelines]) + # create hr + etree.SubElement(parent, 'hr') + # check for lines in block after hr. + postlines = block[match.end():].lstrip('\n') + if postlines: + # Add lines after hr to master blocks for later parsing. + blocks.insert(0, postlines) + + +class EmptyBlockProcessor(BlockProcessor): + """ Process blocks that are empty or start with an empty line. """ + + def test(self, parent, block): + return not block or block.startswith('\n') + + def run(self, parent, blocks): + block = blocks.pop(0) + filler = '\n\n' + if block: + # Starts with empty line + # Only replace a single line. + filler = '\n' + # Save the rest for later. + theRest = block[1:] + if theRest: + # Add remaining lines to master blocks for later. + blocks.insert(0, theRest) + sibling = self.lastChild(parent) + if (sibling is not None and sibling.tag == 'pre' and + len(sibling) and sibling[0].tag == 'code'): + # Last block is a codeblock. Append to preserve whitespace. + sibling[0].text = util.AtomicString( + '{}{}'.format(sibling[0].text, filler) + ) + + +class ReferenceProcessor(BlockProcessor): + """ Process link references. """ + RE = re.compile( + r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE + ) + + def test(self, parent, block): + return True + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + if m: + id = m.group(1).strip().lower() + link = m.group(2).lstrip('<').rstrip('>') + title = m.group(5) or m.group(6) + self.parser.md.references[id] = (link, title) + if block[m.end():].strip(): + # Add any content after match back to blocks as separate block + blocks.insert(0, block[m.end():].lstrip('\n')) + if block[:m.start()].strip(): + # Add any content before match back to blocks as separate block + blocks.insert(0, block[:m.start()].rstrip('\n')) + return True + # No match. Restore block. + blocks.insert(0, block) + return False + + +class ParagraphProcessor(BlockProcessor): + """ Process Paragraph blocks. """ + + def test(self, parent, block): + return True + + def run(self, parent, blocks): + block = blocks.pop(0) + if block.strip(): + # Not a blank block. Add to parent, otherwise throw it away. + if self.parser.state.isstate('list'): + # The parent is a tight-list. + # + # Check for any children. This will likely only happen in a + # tight-list when a header isn't followed by a blank line. + # For example: + # + # * # Header + # Line 2 of list item - not part of header. + sibling = self.lastChild(parent) + if sibling is not None: + # Insetrt after sibling. + if sibling.tail: + sibling.tail = '{}\n{}'.format(sibling.tail, block) + else: + sibling.tail = '\n%s' % block + else: + # Append to parent.text + if parent.text: + parent.text = '{}\n{}'.format(parent.text, block) + else: + parent.text = block.lstrip() + else: + # Create a regular paragraph + p = etree.SubElement(parent, 'p') + p.text = block.lstrip() diff --git a/Libs/markdown/core.py b/Libs/markdown/core.py new file mode 100644 index 0000000..d8c8196 --- /dev/null +++ b/Libs/markdown/core.py @@ -0,0 +1,407 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import codecs +import sys +import logging +import importlib +from . import util +from .preprocessors import build_preprocessors +from .blockprocessors import build_block_parser +from .treeprocessors import build_treeprocessors +from .inlinepatterns import build_inlinepatterns +from .postprocessors import build_postprocessors +from .extensions import Extension +from .serializers import to_html_string, to_xhtml_string + +__all__ = ['Markdown', 'markdown', 'markdownFromFile'] + + +logger = logging.getLogger('MARKDOWN') + + +class Markdown: + """Convert Markdown to HTML.""" + + doc_tag = "div" # Element used to wrap document - later removed + + output_formats = { + 'html': to_html_string, + 'xhtml': to_xhtml_string, + } + + def __init__(self, **kwargs): + """ + Creates a new Markdown instance. + + Keyword arguments: + + * extensions: A list of extensions. + If an item is an instance of a subclass of `markdown.extension.Extension`, the instance will be used + as-is. If an item is of type string, first an entry point will be loaded. If that fails, the string is + assumed to use Python dot notation (`path.to.module:ClassName`) to load a markdown.Extension subclass. If + no class is specified, then a `makeExtension` function is called within the specified module. + * extension_configs: Configuration settings for extensions. + * output_format: Format of output. Supported formats are: + * "xhtml": Outputs XHTML style tags. Default. + * "html": Outputs HTML style tags. + * tab_length: Length of tabs in the source. Default: 4 + + """ + + self.tab_length = kwargs.get('tab_length', 4) + + self.ESCAPED_CHARS = ['\\', '`', '*', '_', '{', '}', '[', ']', + '(', ')', '>', '#', '+', '-', '.', '!'] + + self.block_level_elements = [ + # Elements which are invalid to wrap in a `

` tag. + # See https://w3c.github.io/html/grouping-content.html#the-p-element + 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', + 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', + 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', + 'p', 'pre', 'section', 'table', 'ul', + # Other elements which Markdown should not be mucking up the contents of. + 'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend', + 'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script', + 'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video' + ] + + self.registeredExtensions = [] + self.docType = "" + self.stripTopLevelTags = True + + self.build_parser() + + self.references = {} + self.htmlStash = util.HtmlStash() + self.registerExtensions(extensions=kwargs.get('extensions', []), + configs=kwargs.get('extension_configs', {})) + self.set_output_format(kwargs.get('output_format', 'xhtml')) + self.reset() + + def build_parser(self): + """ Build the parser from the various parts. """ + self.preprocessors = build_preprocessors(self) + self.parser = build_block_parser(self) + self.inlinePatterns = build_inlinepatterns(self) + self.treeprocessors = build_treeprocessors(self) + self.postprocessors = build_postprocessors(self) + return self + + def registerExtensions(self, extensions, configs): + """ + Register extensions with this instance of Markdown. + + Keyword arguments: + + * extensions: A list of extensions, which can either + be strings or objects. + * configs: A dictionary mapping extension names to config options. + + """ + for ext in extensions: + if isinstance(ext, str): + ext = self.build_extension(ext, configs.get(ext, {})) + if isinstance(ext, Extension): + ext._extendMarkdown(self) + logger.debug( + 'Successfully loaded extension "%s.%s".' + % (ext.__class__.__module__, ext.__class__.__name__) + ) + elif ext is not None: + raise TypeError( + 'Extension "{}.{}" must be of type: "{}.{}"'.format( + ext.__class__.__module__, ext.__class__.__name__, + Extension.__module__, Extension.__name__ + ) + ) + return self + + def build_extension(self, ext_name, configs): + """ + Build extension from a string name, then return an instance. + + First attempt to load an entry point. The string name must be registered as an entry point in the + `markdown.extensions` group which points to a subclass of the `markdown.extensions.Extension` class. + If multiple distributions have registered the same name, the first one found is returned. + + If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and + return an instance. If no class is specified, import the module and call a `makeExtension` function and return + the Extension instance returned by that function. + """ + configs = dict(configs) + + entry_points = [ep for ep in util.INSTALLED_EXTENSIONS if ep.name == ext_name] + if entry_points: + ext = entry_points[0].load() + return ext(**configs) + + # Get class name (if provided): `path.to.module:ClassName` + ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '') + + try: + module = importlib.import_module(ext_name) + logger.debug( + 'Successfully imported extension module "%s".' % ext_name + ) + except ImportError as e: + message = 'Failed loading extension "%s".' % ext_name + e.args = (message,) + e.args[1:] + raise + + if class_name: + # Load given class name from module. + return getattr(module, class_name)(**configs) + else: + # Expect makeExtension() function to return a class. + try: + return module.makeExtension(**configs) + except AttributeError as e: + message = e.args[0] + message = "Failed to initiate extension " \ + "'%s': %s" % (ext_name, message) + e.args = (message,) + e.args[1:] + raise + + def registerExtension(self, extension): + """ This gets called by the extension """ + self.registeredExtensions.append(extension) + return self + + def reset(self): + """ + Resets all state variables so that we can start with a new text. + """ + self.htmlStash.reset() + self.references.clear() + + for extension in self.registeredExtensions: + if hasattr(extension, 'reset'): + extension.reset() + + return self + + def set_output_format(self, format): + """ Set the output format for the class instance. """ + self.output_format = format.lower().rstrip('145') # ignore num + try: + self.serializer = self.output_formats[self.output_format] + except KeyError as e: + valid_formats = list(self.output_formats.keys()) + valid_formats.sort() + message = 'Invalid Output Format: "%s". Use one of %s.' \ + % (self.output_format, + '"' + '", "'.join(valid_formats) + '"') + e.args = (message,) + e.args[1:] + raise + return self + + def is_block_level(self, tag): + """Check if the tag is a block level HTML tag.""" + if isinstance(tag, str): + return tag.lower().rstrip('/') in self.block_level_elements + # Some ElementTree tags are not strings, so return False. + return False + + def convert(self, source): + """ + Convert markdown to serialized XHTML or HTML. + + Keyword arguments: + + * source: Source text as a Unicode string. + + Markdown processing takes place in five steps: + + 1. A bunch of "preprocessors" munge the input text. + 2. BlockParser() parses the high-level structural elements of the + pre-processed text into an ElementTree. + 3. A bunch of "treeprocessors" are run against the ElementTree. One + such treeprocessor runs InlinePatterns against the ElementTree, + detecting inline markup. + 4. Some post-processors are run against the text after the ElementTree + has been serialized into text. + 5. The output is written to a string. + + """ + + # Fixup the source text + if not source.strip(): + return '' # a blank unicode string + + try: + source = str(source) + except UnicodeDecodeError as e: # pragma: no cover + # Customise error message while maintaining original trackback + e.reason += '. -- Note: Markdown only accepts unicode input!' + raise + + # Split into lines and run the line preprocessors. + self.lines = source.split("\n") + for prep in self.preprocessors: + self.lines = prep.run(self.lines) + + # Parse the high-level elements. + root = self.parser.parseDocument(self.lines).getroot() + + # Run the tree-processors + for treeprocessor in self.treeprocessors: + newRoot = treeprocessor.run(root) + if newRoot is not None: + root = newRoot + + # Serialize _properly_. Strip top-level tags. + output = self.serializer(root) + if self.stripTopLevelTags: + try: + start = output.index( + '<%s>' % self.doc_tag) + len(self.doc_tag) + 2 + end = output.rindex('' % self.doc_tag) + output = output[start:end].strip() + except ValueError as e: # pragma: no cover + if output.strip().endswith('<%s />' % self.doc_tag): + # We have an empty document + output = '' + else: + # We have a serious problem + raise ValueError('Markdown failed to strip top-level ' + 'tags. Document=%r' % output.strip()) from e + + # Run the text post-processors + for pp in self.postprocessors: + output = pp.run(output) + + return output.strip() + + def convertFile(self, input=None, output=None, encoding=None): + """Converts a markdown file and returns the HTML as a unicode string. + + Decodes the file using the provided encoding (defaults to utf-8), + passes the file content to markdown, and outputs the html to either + the provided stream or the file with provided name, using the same + encoding as the source file. The 'xmlcharrefreplace' error handler is + used when encoding the output. + + **Note:** This is the only place that decoding and encoding of unicode + takes place in Python-Markdown. (All other code is unicode-in / + unicode-out.) + + Keyword arguments: + + * input: File object or path. Reads from stdin if `None`. + * output: File object or path. Writes to stdout if `None`. + * encoding: Encoding of input and output files. Defaults to utf-8. + + """ + + encoding = encoding or "utf-8" + + # Read the source + if input: + if isinstance(input, str): + input_file = codecs.open(input, mode="r", encoding=encoding) + else: + input_file = codecs.getreader(encoding)(input) + text = input_file.read() + input_file.close() + else: + text = sys.stdin.read() + if not isinstance(text, str): # pragma: no cover + text = text.decode(encoding) + + text = text.lstrip('\ufeff') # remove the byte-order mark + + # Convert + html = self.convert(text) + + # Write to file or stdout + if output: + if isinstance(output, str): + output_file = codecs.open(output, "w", + encoding=encoding, + errors="xmlcharrefreplace") + output_file.write(html) + output_file.close() + else: + writer = codecs.getwriter(encoding) + output_file = writer(output, errors="xmlcharrefreplace") + output_file.write(html) + # Don't close here. User may want to write more. + else: + # Encode manually and write bytes to stdout. + html = html.encode(encoding, "xmlcharrefreplace") + try: + # Write bytes directly to buffer (Python 3). + sys.stdout.buffer.write(html) + except AttributeError: # pragma: no cover + # Probably Python 2, which works with bytes by default. + sys.stdout.write(html) + + return self + + +""" +EXPORTED FUNCTIONS +============================================================================= + +Those are the two functions we really mean to export: markdown() and +markdownFromFile(). +""" + + +def markdown(text, **kwargs): + """Convert a markdown string to HTML and return HTML as a unicode string. + + This is a shortcut function for `Markdown` class to cover the most + basic use case. It initializes an instance of Markdown, loads the + necessary extensions and runs the parser on the given text. + + Keyword arguments: + + * text: Markdown formatted text as Unicode or ASCII string. + * Any arguments accepted by the Markdown class. + + Returns: An HTML document as a string. + + """ + md = Markdown(**kwargs) + return md.convert(text) + + +def markdownFromFile(**kwargs): + """Read markdown code from a file and write it to a file or a stream. + + This is a shortcut function which initializes an instance of Markdown, + and calls the convertFile method rather than convert. + + Keyword arguments: + + * input: a file name or readable object. + * output: a file name or writable object. + * encoding: Encoding of input and output. + * Any arguments accepted by the Markdown class. + + """ + md = Markdown(**kwargs) + md.convertFile(kwargs.get('input', None), + kwargs.get('output', None), + kwargs.get('encoding', None)) diff --git a/Libs/markdown/extensions/__init__.py b/Libs/markdown/extensions/__init__.py new file mode 100644 index 0000000..4712e25 --- /dev/null +++ b/Libs/markdown/extensions/__init__.py @@ -0,0 +1,107 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import warnings +from ..util import parseBoolValue + + +class Extension: + """ Base class for extensions to subclass. """ + + # Default config -- to be overridden by a subclass + # Must be of the following format: + # { + # 'key': ['value', 'description'] + # } + # Note that Extension.setConfig will raise a KeyError + # if a default is not set here. + config = {} + + def __init__(self, **kwargs): + """ Initiate Extension and set up configs. """ + self.setConfigs(kwargs) + + def getConfig(self, key, default=''): + """ Return a setting for the given key or an empty string. """ + if key in self.config: + return self.config[key][0] + else: + return default + + def getConfigs(self): + """ Return all configs settings as a dict. """ + return {key: self.getConfig(key) for key in self.config.keys()} + + def getConfigInfo(self): + """ Return all config descriptions as a list of tuples. """ + return [(key, self.config[key][1]) for key in self.config.keys()] + + def setConfig(self, key, value): + """ Set a config setting for `key` with the given `value`. """ + if isinstance(self.config[key][0], bool): + value = parseBoolValue(value) + if self.config[key][0] is None: + value = parseBoolValue(value, preserve_none=True) + self.config[key][0] = value + + def setConfigs(self, items): + """ Set multiple config settings given a dict or list of tuples. """ + if hasattr(items, 'items'): + # it's a dict + items = items.items() + for key, value in items: + self.setConfig(key, value) + + def _extendMarkdown(self, *args): + """ Private wrapper around extendMarkdown. """ + md = args[0] + try: + self.extendMarkdown(md) + except TypeError as e: + if "missing 1 required positional argument" in str(e): + # Must be a 2.x extension. Pass in a dumby md_globals. + self.extendMarkdown(md, {}) + warnings.warn( + "The 'md_globals' parameter of '{}.{}.extendMarkdown' is " + "deprecated.".format(self.__class__.__module__, self.__class__.__name__), + category=DeprecationWarning, + stacklevel=2 + ) + else: + raise + + def extendMarkdown(self, md): + """ + Add the various processors and patterns to the Markdown Instance. + + This method must be overridden by every extension. + + Keyword arguments: + + * md: The Markdown instance. + + * md_globals: Global variables in the markdown module namespace. + + """ + raise NotImplementedError( + 'Extension "%s.%s" must define an "extendMarkdown"' + 'method.' % (self.__class__.__module__, self.__class__.__name__) + ) diff --git a/Libs/markdown/extensions/__pycache__/__init__.cpython-310.pyc b/Libs/markdown/extensions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00d7ee9588531083d5f424faa0f1d13fa35f0a3c GIT binary patch literal 4004 zcmZ`+TW=f36`mbZ61A2TM|M)HNQ0>X$7X6TCAYp*P>tG>;i|D!qcUPdZ9p$MLvrQi zE~M*zcb65WM#>jo!yx^bIy0Jn=UT;4A(!4 zw)Tf5#{NSO(^mlxzeO{jqLXZoNl)_F+w){W7GvHn>=iurfXNv-`-;igmwc}%`7SHZ zg&Akef84jNG!c(N{amKK#P{!t;j5_KiB(%AHnb5|qz&==w3Ublx?5BFvKem)e19vg zcgHK&Ma$ZbS+7(&j`p?okap0FY^z%fqO{^ai0tpWwaL=qR6T81^5d_KII4`cSHSDz z?a7u-zY_(iZdN)v-B)#68Sn7@T^nkvq^R}5X423Y?%GHt#_CX9A^yN#uh$D?Pw8fm zC|fB9{-ZF0v{8c9jZ}+JG*nOYRj3US`l72N@g#iSvZ}8Rj#0q@suvlVwuvE7+U#l# zbfnhVJK}b!B-*ZdKSN*MaWOZNcw!(TV869|HF_!eh8@I|L zOr+R0VOSTBx<;K!xDnP>tc=0D28;e?+Ue`4*|OsL>gug)*H_nWizh8LQvV1|@i>Q9 zTsa8DT5!u96^9l?j+6M+RU&l5^*7%^3!7Ji>(|zTo3)esPE(NuBPqCxk}wtrN*kD9 z+4mntbtqA-i+j6U;)+p9Jlx*Azq5NkXy-bU3JQ!3 z^DyqJI!#VrV=wv~Zxw_0ustrly+XvuUu5AG+nbT+ofmm~vvN+B@MccVOCR@=JR=uy zpO=er33p$S@x5nkEoC zK*h()&HP1~XGNppMmGDr7wV>Y!*mJs|F=(*Ab)Ip__8tWb&Mm00>%?bCF zYBd4})oRe`XQk1@C&Vyuzr+X2W5$BfbjIo=*65-!E#5>kAE7(+4%t3CV0wY>hu#Xr zU17WIkRll$+!lXSwyP5n3P@z5r0F0fBWgwmDiP0BALaxcglczeUmy-(Q6Myd`^L*= zb6Cm+J&PAv0qe6lsTyH7who89o2SY7<_I*3xP~_Pe90^F!MTZG<2A?BN2if&)V{lb z?jNk-QM`Ut`WkY5>%C@M>?J9(07Z^uisO;iM$#D(;xA-Wx2G5gadZyN&75G7&HmPGdYGs>tidd!T;ruEPHQBuX=&H zW$GqE=JIXOaSp*-n05$$%m%+WMZWE%kxqd)-lso2PIm&{;ut*IdxMs(pd0+;N2tH! zXGN^uL^BW3SxCAMTgm;)@|RpM*}@@z=^gRu8Ogg$|BPm+%J_z3c4~fuxe8+jez}XN z6AsOHWWCNepTQiSi5@BfF>%vp2Nay24gGTG2}0CjM=K<(1657ZM3sw~ADIYwJWT3p zXjnBwbxkfcbYEx4*^Hx=+ZshOojdP1;{BRrrqb#>FL}$}0@}0#kJr`797Y#`MW*2Z zTg3o?!GQtO{GmtgOGdKAzV!bLNRA4K&IC;1HTT$yGY*u(;En^OjmQ$g_)cJBh5;*g zZi-naO*NZ|tZJK`av_^*g(kFC>x1J}9|N#3>U!MVYzFip+Zb-+1h(;IY9a$t`er z=^=!#a|qMMI;UJoTK zg5+Zz9Z<6C>9EsL2=O}Yq83t=P35SD85-2!lXhfaJx#GDw0hBvs4hf8L%vL@&OqLC zW$a*+>Y&SQS#8E?4e{)OjRiWj|XwzCW9P((kDITBkZIAU|a#+j-jOEqAsVpw*wzg8lfF^6UCB|)=>%NaRSBe zh}4r7v;;-AQYqS9B!L?0Ow^Y;k`i?SN_40PRkwqJ3bm=dpQ(N?)pBemW_?2UQ_+^| zu6!P!K8jLP6lm4Rrq;y+XIz4$3TsG1B1#q^E{AHLOx+AnrgW} zp}FwE7dcFW|A6puVa(DA*FdSM&k)f?N86o}EmW&7x?w!D*|};}ruAyog>+3J##JVo z%tTWK=)#+eT$;)d$HWw4li)@c_ER)NDYL|iUU9y}{bFgZRKyS0!m_3{&kk@ +for documentation. + +Oringinal code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/) and + [Seemant Kulleen](http://www.kulleen.org/) + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +''' + +from . import Extension +from ..blockprocessors import BlockProcessor +from ..inlinepatterns import InlineProcessor +from ..util import AtomicString +import re +import xml.etree.ElementTree as etree + + +class AbbrExtension(Extension): + """ Abbreviation Extension for Python-Markdown. """ + + def extendMarkdown(self, md): + """ Insert AbbrPreprocessor before ReferencePreprocessor. """ + md.parser.blockprocessors.register(AbbrPreprocessor(md.parser), 'abbr', 16) + + +class AbbrPreprocessor(BlockProcessor): + """ Abbreviation Preprocessor - parse text for abbr references. """ + + RE = re.compile(r'^[*]\[(?P[^\]]*)\][ ]?:[ ]*\n?[ ]*(?P.*)$', re.MULTILINE) + + def test(self, parent, block): + return True + + def run(self, parent, blocks): + ''' + Find and remove all Abbreviation references from the text. + Each reference is set as a new AbbrPattern in the markdown instance. + + ''' + block = blocks.pop(0) + m = self.RE.search(block) + if m: + abbr = m.group('abbr').strip() + title = m.group('title').strip() + self.parser.md.inlinePatterns.register( + AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2 + ) + if block[m.end():].strip(): + # Add any content after match back to blocks as separate block + blocks.insert(0, block[m.end():].lstrip('\n')) + if block[:m.start()].strip(): + # Add any content before match back to blocks as separate block + blocks.insert(0, block[:m.start()].rstrip('\n')) + return True + # No match. Restore block. + blocks.insert(0, block) + return False + + def _generate_pattern(self, text): + ''' + Given a string, returns an regex pattern to match that string. + + 'HTML' -> r'(?P<abbr>[H][T][M][L])' + + Note: we force each char as a literal match (in brackets) as we don't + know what they will be beforehand. + + ''' + chars = list(text) + for i in range(len(chars)): + chars[i] = r'[%s]' % chars[i] + return r'(?P<abbr>\b%s\b)' % (r''.join(chars)) + + +class AbbrInlineProcessor(InlineProcessor): + """ Abbreviation inline pattern. """ + + def __init__(self, pattern, title): + super().__init__(pattern) + self.title = title + + def handleMatch(self, m, data): + abbr = etree.Element('abbr') + abbr.text = AtomicString(m.group('abbr')) + abbr.set('title', self.title) + return abbr, m.start(0), m.end(0) + + +def makeExtension(**kwargs): # pragma: no cover + return AbbrExtension(**kwargs) diff --git a/Libs/markdown/extensions/admonition.py b/Libs/markdown/extensions/admonition.py new file mode 100644 index 0000000..cb8d901 --- /dev/null +++ b/Libs/markdown/extensions/admonition.py @@ -0,0 +1,170 @@ +""" +Admonition extension for Python-Markdown +======================================== + +Adds rST-style admonitions. Inspired by [rST][] feature with the same name. + +[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions # noqa + +See <https://Python-Markdown.github.io/extensions/admonition> +for documentation. + +Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/). + +All changes Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import BlockProcessor +import xml.etree.ElementTree as etree +import re + + +class AdmonitionExtension(Extension): + """ Admonition extension for Python-Markdown. """ + + def extendMarkdown(self, md): + """ Add Admonition to Markdown instance. """ + md.registerExtension(self) + + md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105) + + +class AdmonitionProcessor(BlockProcessor): + + CLASSNAME = 'admonition' + CLASSNAME_TITLE = 'admonition-title' + RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)') + RE_SPACES = re.compile(' +') + + def __init__(self, parser): + """Initialization.""" + + super().__init__(parser) + + self.current_sibling = None + self.content_indention = 0 + + def parse_content(self, parent, block): + """Get sibling admonition. + + Retrieve the appropriate sibling element. This can get tricky when + dealing with lists. + + """ + + old_block = block + the_rest = '' + + # We already acquired the block via test + if self.current_sibling is not None: + sibling = self.current_sibling + block, the_rest = self.detab(block, self.content_indent) + self.current_sibling = None + self.content_indent = 0 + return sibling, block, the_rest + + sibling = self.lastChild(parent) + + if sibling is None or sibling.get('class', '').find(self.CLASSNAME) == -1: + sibling = None + else: + # If the last child is a list and the content is sufficiently indented + # to be under it, then the content's sibling is in the list. + last_child = self.lastChild(sibling) + indent = 0 + while last_child: + if ( + sibling and block.startswith(' ' * self.tab_length * 2) and + last_child and last_child.tag in ('ul', 'ol', 'dl') + ): + + # The expectation is that we'll find an <li> or <dt>. + # We should get its last child as well. + sibling = self.lastChild(last_child) + last_child = self.lastChild(sibling) if sibling else None + + # Context has been lost at this point, so we must adjust the + # text's indentation level so it will be evaluated correctly + # under the list. + block = block[self.tab_length:] + indent += self.tab_length + else: + last_child = None + + if not block.startswith(' ' * self.tab_length): + sibling = None + + if sibling is not None: + indent += self.tab_length + block, the_rest = self.detab(old_block, indent) + self.current_sibling = sibling + self.content_indent = indent + + return sibling, block, the_rest + + def test(self, parent, block): + + if self.RE.search(block): + return True + else: + return self.parse_content(parent, block)[0] is not None + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + + if m: + if m.start() > 0: + self.parser.parseBlocks(parent, [block[:m.start()]]) + block = block[m.end():] # removes the first line + block, theRest = self.detab(block) + else: + sibling, block, theRest = self.parse_content(parent, block) + + if m: + klass, title = self.get_class_and_title(m) + div = etree.SubElement(parent, 'div') + div.set('class', '{} {}'.format(self.CLASSNAME, klass)) + if title: + p = etree.SubElement(div, 'p') + p.text = title + p.set('class', self.CLASSNAME_TITLE) + else: + # Sibling is a list item, but we need to wrap it's content should be wrapped in <p> + if sibling.tag in ('li', 'dd') and sibling.text: + text = sibling.text + sibling.text = '' + p = etree.SubElement(sibling, 'p') + p.text = text + + div = sibling + + self.parser.parseChunk(div, block) + + if theRest: + # This block contained unindented line(s) after the first indented + # line. Insert these lines as the first block of the master blocks + # list for future processing. + blocks.insert(0, theRest) + + def get_class_and_title(self, match): + klass, title = match.group(1).lower(), match.group(2) + klass = self.RE_SPACES.sub(' ', klass) + if title is None: + # no title was provided, use the capitalized classname as title + # e.g.: `!!! note` will render + # `<p class="admonition-title">Note</p>` + title = klass.split(' ', 1)[0].capitalize() + elif title == '': + # an explicit blank title should not be rendered + # e.g.: `!!! warning ""` will *not* render `p` with a title + title = None + return klass, title + + +def makeExtension(**kwargs): # pragma: no cover + return AdmonitionExtension(**kwargs) diff --git a/Libs/markdown/extensions/attr_list.py b/Libs/markdown/extensions/attr_list.py new file mode 100644 index 0000000..9a67551 --- /dev/null +++ b/Libs/markdown/extensions/attr_list.py @@ -0,0 +1,166 @@ +""" +Attribute List Extension for Python-Markdown +============================================ + +Adds attribute list syntax. Inspired by +[maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s +feature of the same name. + +See <https://Python-Markdown.github.io/extensions/attr_list> +for documentation. + +Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/). + +All changes Copyright 2011-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..treeprocessors import Treeprocessor +import re + + +def _handle_double_quote(s, t): + k, v = t.split('=', 1) + return k, v.strip('"') + + +def _handle_single_quote(s, t): + k, v = t.split('=', 1) + return k, v.strip("'") + + +def _handle_key_value(s, t): + return t.split('=', 1) + + +def _handle_word(s, t): + if t.startswith('.'): + return '.', t[1:] + if t.startswith('#'): + return 'id', t[1:] + return t, t + + +_scanner = re.Scanner([ + (r'[^ =]+=".*?"', _handle_double_quote), + (r"[^ =]+='.*?'", _handle_single_quote), + (r'[^ =]+=[^ =]+', _handle_key_value), + (r'[^ =]+', _handle_word), + (r' ', None) +]) + + +def get_attrs(str): + """ Parse attribute list and return a list of attribute tuples. """ + return _scanner.scan(str)[0] + + +def isheader(elem): + return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] + + +class AttrListTreeprocessor(Treeprocessor): + + BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}' + HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE)) + BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE)) + INLINE_RE = re.compile(r'^{}'.format(BASE_RE)) + NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff' + r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d' + r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' + r'\uf900-\ufdcf\ufdf0-\ufffd' + r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+') + + def run(self, doc): + for elem in doc.iter(): + if self.md.is_block_level(elem.tag): + # Block level: check for attrs on last line of text + RE = self.BLOCK_RE + if isheader(elem) or elem.tag in ['dt', 'td', 'th']: + # header, def-term, or table cell: check for attrs at end of element + RE = self.HEADER_RE + if len(elem) and elem.tag == 'li': + # special case list items. children may include a ul or ol. + pos = None + # find the ul or ol position + for i, child in enumerate(elem): + if child.tag in ['ul', 'ol']: + pos = i + break + if pos is None and elem[-1].tail: + # use tail of last child. no ul or ol. + m = RE.search(elem[-1].tail) + if m: + self.assign_attrs(elem, m.group(1)) + elem[-1].tail = elem[-1].tail[:m.start()] + elif pos is not None and pos > 0 and elem[pos-1].tail: + # use tail of last child before ul or ol + m = RE.search(elem[pos-1].tail) + if m: + self.assign_attrs(elem, m.group(1)) + elem[pos-1].tail = elem[pos-1].tail[:m.start()] + elif elem.text: + # use text. ul is first child. + m = RE.search(elem.text) + if m: + self.assign_attrs(elem, m.group(1)) + elem.text = elem.text[:m.start()] + elif len(elem) and elem[-1].tail: + # has children. Get from tail of last child + m = RE.search(elem[-1].tail) + if m: + self.assign_attrs(elem, m.group(1)) + elem[-1].tail = elem[-1].tail[:m.start()] + if isheader(elem): + # clean up trailing #s + elem[-1].tail = elem[-1].tail.rstrip('#').rstrip() + elif elem.text: + # no children. Get from text. + m = RE.search(elem.text) + if m: + self.assign_attrs(elem, m.group(1)) + elem.text = elem.text[:m.start()] + if isheader(elem): + # clean up trailing #s + elem.text = elem.text.rstrip('#').rstrip() + else: + # inline: check for attrs at start of tail + if elem.tail: + m = self.INLINE_RE.match(elem.tail) + if m: + self.assign_attrs(elem, m.group(1)) + elem.tail = elem.tail[m.end():] + + def assign_attrs(self, elem, attrs): + """ Assign attrs to element. """ + for k, v in get_attrs(attrs): + if k == '.': + # add to class + cls = elem.get('class') + if cls: + elem.set('class', '{} {}'.format(cls, v)) + else: + elem.set('class', v) + else: + # assign attr k with v + elem.set(self.sanitize_name(k), v) + + def sanitize_name(self, name): + """ + Sanitize name as 'an XML Name, minus the ":"'. + See https://www.w3.org/TR/REC-xml-names/#NT-NCName + """ + return self.NAME_RE.sub('_', name) + + +class AttrListExtension(Extension): + def extendMarkdown(self, md): + md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8) + md.registerExtension(self) + + +def makeExtension(**kwargs): # pragma: no cover + return AttrListExtension(**kwargs) diff --git a/Libs/markdown/extensions/codehilite.py b/Libs/markdown/extensions/codehilite.py new file mode 100644 index 0000000..a768b73 --- /dev/null +++ b/Libs/markdown/extensions/codehilite.py @@ -0,0 +1,308 @@ +""" +CodeHilite Extension for Python-Markdown +======================================== + +Adds code/syntax highlighting to standard Python-Markdown code blocks. + +See <https://Python-Markdown.github.io/extensions/code_hilite> +for documentation. + +Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/). + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..treeprocessors import Treeprocessor +from ..util import parseBoolValue + +try: # pragma: no cover + from pygments import highlight + from pygments.lexers import get_lexer_by_name, guess_lexer + from pygments.formatters import get_formatter_by_name + pygments = True +except ImportError: # pragma: no cover + pygments = False + + +def parse_hl_lines(expr): + """Support our syntax for emphasizing certain lines of code. + + expr should be like '1 2' to emphasize lines 1 and 2 of a code block. + Returns a list of ints, the line numbers to emphasize. + """ + if not expr: + return [] + + try: + return list(map(int, expr.split())) + except ValueError: # pragma: no cover + return [] + + +# ------------------ The Main CodeHilite Class ---------------------- +class CodeHilite: + """ + Determine language of source code, and pass it on to the Pygments highlighter. + + Usage: + code = CodeHilite(src=some_code, lang='python') + html = code.hilite() + + Arguments: + * src: Source string or any object with a .readline attribute. + + * lang: String name of Pygments lexer to use for highlighting. Default: `None`. + + * guess_lang: Auto-detect which lexer to use. Ignored if `lang` is set to a valid + value. Default: `True`. + + * use_pygments: Pass code to pygments for code highlighting. If `False`, the code is + instead wrapped for highlighting by a JavaScript library. Default: `True`. + + * linenums: An alias to Pygments `linenos` formatter option. Default: `None`. + + * css_class: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'. + + * lang_prefix: Prefix prepended to the language when `use_pygments` is `False`. + Default: "language-". + + Other Options: + Any other options are accepted and passed on to the lexer and formatter. Therefore, + valid options include any options which are accepted by the `html` formatter or + whichever lexer the code's language uses. Note that most lexers do not have any + options. However, a few have very useful options, such as PHP's `startinline` option. + Any invalid options are ignored without error. + + Formatter options: https://pygments.org/docs/formatters/#HtmlFormatter + Lexer Options: https://pygments.org/docs/lexers/ + + Advanced Usage: + code = CodeHilite( + src = some_code, + lang = 'php', + startinline = True, # Lexer option. Snippet does not start with `<?php`. + linenostart = 42, # Formatter option. Snippet starts on line 42. + hl_lines = [45, 49, 50], # Formatter option. Highlight lines 45, 49, and 50. + linenos = 'inline' # Formatter option. Avoid alignment problems. + ) + html = code.hilite() + + """ + + def __init__(self, src, **options): + self.src = src + self.lang = options.pop('lang', None) + self.guess_lang = options.pop('guess_lang', True) + self.use_pygments = options.pop('use_pygments', True) + self.lang_prefix = options.pop('lang_prefix', 'language-') + + if 'linenos' not in options: + options['linenos'] = options.pop('linenums', None) + if 'cssclass' not in options: + options['cssclass'] = options.pop('css_class', 'codehilite') + if 'wrapcode' not in options: + # Override pygments default + options['wrapcode'] = True + # Disallow use of `full` option + options['full'] = False + + self.options = options + + def hilite(self, shebang=True): + """ + Pass code to the [Pygments](http://pygments.pocoo.org/) highliter with + optional line numbers. The output should then be styled with css to + your liking. No styles are applied by default - only styling hooks + (i.e.: <span class="k">). + + returns : A string of html. + + """ + + self.src = self.src.strip('\n') + + if self.lang is None and shebang: + self._parseHeader() + + if pygments and self.use_pygments: + try: + lexer = get_lexer_by_name(self.lang, **self.options) + except ValueError: + try: + if self.guess_lang: + lexer = guess_lexer(self.src, **self.options) + else: + lexer = get_lexer_by_name('text', **self.options) + except ValueError: # pragma: no cover + lexer = get_lexer_by_name('text', **self.options) + formatter = get_formatter_by_name('html', **self.options) + return highlight(self.src, lexer, formatter) + else: + # just escape and build markup usable by JS highlighting libs + txt = self.src.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + txt = txt.replace('"', '"') + classes = [] + if self.lang: + classes.append('{}{}'.format(self.lang_prefix, self.lang)) + if self.options['linenos']: + classes.append('linenums') + class_str = '' + if classes: + class_str = ' class="{}"'.format(' '.join(classes)) + return '<pre class="{}"><code{}>{}\n</code></pre>\n'.format( + self.options['cssclass'], + class_str, + txt + ) + + def _parseHeader(self): + """ + Determines language of a code block from shebang line and whether the + said line should be removed or left in place. If the sheband line + contains a path (even a single /) then it is assumed to be a real + shebang line and left alone. However, if no path is given + (e.i.: #!python or :::python) then it is assumed to be a mock shebang + for language identification of a code fragment and removed from the + code block prior to processing for code highlighting. When a mock + shebang (e.i: #!python) is found, line numbering is turned on. When + colons are found in place of a shebang (e.i.: :::python), line + numbering is left in the current state - off by default. + + Also parses optional list of highlight lines, like: + + :::python hl_lines="1 3" + """ + + import re + + # split text into lines + lines = self.src.split("\n") + # pull first line to examine + fl = lines.pop(0) + + c = re.compile(r''' + (?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons + (?P<path>(?:/\w+)*[/ ])? # Zero or 1 path + (?P<lang>[\w#.+-]*) # The language + \s* # Arbitrary whitespace + # Optional highlight lines, single- or double-quote-delimited + (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))? + ''', re.VERBOSE) + # search first line for shebang + m = c.search(fl) + if m: + # we have a match + try: + self.lang = m.group('lang').lower() + except IndexError: # pragma: no cover + self.lang = None + if m.group('path'): + # path exists - restore first line + lines.insert(0, fl) + if self.options['linenos'] is None and m.group('shebang'): + # Overridable and Shebang exists - use line numbers + self.options['linenos'] = True + + self.options['hl_lines'] = parse_hl_lines(m.group('hl_lines')) + else: + # No match + lines.insert(0, fl) + + self.src = "\n".join(lines).strip("\n") + + +# ------------------ The Markdown Extension ------------------------------- + + +class HiliteTreeprocessor(Treeprocessor): + """ Highlight source code in code blocks. """ + + def code_unescape(self, text): + """Unescape code.""" + text = text.replace("<", "<") + text = text.replace(">", ">") + # Escaped '&' should be replaced at the end to avoid + # conflicting with < and >. + text = text.replace("&", "&") + return text + + def run(self, root): + """ Find code blocks and store in htmlStash. """ + blocks = root.iter('pre') + for block in blocks: + if len(block) == 1 and block[0].tag == 'code': + local_config = self.config.copy() + code = CodeHilite( + self.code_unescape(block[0].text), + tab_length=self.md.tab_length, + style=local_config.pop('pygments_style', 'default'), + **local_config + ) + placeholder = self.md.htmlStash.store(code.hilite()) + # Clear codeblock in etree instance + block.clear() + # Change to p element which will later + # be removed when inserting raw html + block.tag = 'p' + block.text = placeholder + + +class CodeHiliteExtension(Extension): + """ Add source code highlighting to markdown codeblocks. """ + + def __init__(self, **kwargs): + # define default configs + self.config = { + 'linenums': [None, + "Use lines numbers. True|table|inline=yes, False=no, None=auto"], + 'guess_lang': [True, + "Automatic language detection - Default: True"], + 'css_class': ["codehilite", + "Set class name for wrapper <div> - " + "Default: codehilite"], + 'pygments_style': ['default', + 'Pygments HTML Formatter Style ' + '(Colorscheme) - Default: default'], + 'noclasses': [False, + 'Use inline styles instead of CSS classes - ' + 'Default false'], + 'use_pygments': [True, + 'Use Pygments to Highlight code blocks. ' + 'Disable if using a JavaScript library. ' + 'Default: True'], + 'lang_prefix': [ + 'language-', + 'Prefix prepended to the language when use_pygments is false. Default: "language-"' + ] + } + + for key, value in kwargs.items(): + if key in self.config: + self.setConfig(key, value) + else: + # manually set unknown keywords. + if isinstance(value, str): + try: + # Attempt to parse str as a bool value + value = parseBoolValue(value, preserve_none=True) + except ValueError: + pass # Assume it's not a bool value. Use as-is. + self.config[key] = [value, ''] + + def extendMarkdown(self, md): + """ Add HilitePostprocessor to Markdown instance. """ + hiliter = HiliteTreeprocessor(md) + hiliter.config = self.getConfigs() + md.treeprocessors.register(hiliter, 'hilite', 30) + + md.registerExtension(self) + + +def makeExtension(**kwargs): # pragma: no cover + return CodeHiliteExtension(**kwargs) diff --git a/Libs/markdown/extensions/def_list.py b/Libs/markdown/extensions/def_list.py new file mode 100644 index 0000000..17549f0 --- /dev/null +++ b/Libs/markdown/extensions/def_list.py @@ -0,0 +1,111 @@ +""" +Definition List Extension for Python-Markdown +============================================= + +Adds parsing of Definition Lists to Python-Markdown. + +See <https://Python-Markdown.github.io/extensions/definition_lists> +for documentation. + +Original code Copyright 2008 [Waylan Limberg](http://achinghead.com) + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import BlockProcessor, ListIndentProcessor +import xml.etree.ElementTree as etree +import re + + +class DefListProcessor(BlockProcessor): + """ Process Definition Lists. """ + + RE = re.compile(r'(^|\n)[ ]{0,3}:[ ]{1,3}(.*?)(\n|$)') + NO_INDENT_RE = re.compile(r'^[ ]{0,3}[^ :]') + + def test(self, parent, block): + return bool(self.RE.search(block)) + + def run(self, parent, blocks): + + raw_block = blocks.pop(0) + m = self.RE.search(raw_block) + terms = [term.strip() for term in + raw_block[:m.start()].split('\n') if term.strip()] + block = raw_block[m.end():] + no_indent = self.NO_INDENT_RE.match(block) + if no_indent: + d, theRest = (block, None) + else: + d, theRest = self.detab(block) + if d: + d = '{}\n{}'.format(m.group(2), d) + else: + d = m.group(2) + sibling = self.lastChild(parent) + if not terms and sibling is None: + # This is not a definition item. Most likely a paragraph that + # starts with a colon at the beginning of a document or list. + blocks.insert(0, raw_block) + return False + if not terms and sibling.tag == 'p': + # The previous paragraph contains the terms + state = 'looselist' + terms = sibling.text.split('\n') + parent.remove(sibling) + # Acquire new sibling + sibling = self.lastChild(parent) + else: + state = 'list' + + if sibling is not None and sibling.tag == 'dl': + # This is another item on an existing list + dl = sibling + if not terms and len(dl) and dl[-1].tag == 'dd' and len(dl[-1]): + state = 'looselist' + else: + # This is a new list + dl = etree.SubElement(parent, 'dl') + # Add terms + for term in terms: + dt = etree.SubElement(dl, 'dt') + dt.text = term + # Add definition + self.parser.state.set(state) + dd = etree.SubElement(dl, 'dd') + self.parser.parseBlocks(dd, [d]) + self.parser.state.reset() + + if theRest: + blocks.insert(0, theRest) + + +class DefListIndentProcessor(ListIndentProcessor): + """ Process indented children of definition list items. """ + + # Definition lists need to be aware of all list types + ITEM_TYPES = ['dd', 'li'] + LIST_TYPES = ['dl', 'ol', 'ul'] + + def create_item(self, parent, block): + """ Create a new dd or li (depending on parent) and parse the block with it as the parent. """ + + dd = etree.SubElement(parent, 'dd') + self.parser.parseBlocks(dd, [block]) + + +class DefListExtension(Extension): + """ Add definition lists to Markdown. """ + + def extendMarkdown(self, md): + """ Add an instance of DefListProcessor to BlockParser. """ + md.parser.blockprocessors.register(DefListIndentProcessor(md.parser), 'defindent', 85) + md.parser.blockprocessors.register(DefListProcessor(md.parser), 'deflist', 25) + + +def makeExtension(**kwargs): # pragma: no cover + return DefListExtension(**kwargs) diff --git a/Libs/markdown/extensions/extra.py b/Libs/markdown/extensions/extra.py new file mode 100644 index 0000000..909ba07 --- /dev/null +++ b/Libs/markdown/extensions/extra.py @@ -0,0 +1,58 @@ +""" +Python-Markdown Extra Extension +=============================== + +A compilation of various Python-Markdown extensions that imitates +[PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/). + +Note that each of the individual extensions still need to be available +on your PYTHONPATH. This extension simply wraps them all up as a +convenience so that only one extension needs to be listed when +initiating Markdown. See the documentation for each individual +extension for specifics about that extension. + +There may be additional extensions that are distributed with +Python-Markdown that are not included here in Extra. Those extensions +are not part of PHP Markdown Extra, and therefore, not part of +Python-Markdown Extra. If you really would like Extra to include +additional extensions, we suggest creating your own clone of Extra +under a different name. You could also edit the `extensions` global +variable defined below, but be aware that such changes may be lost +when you upgrade to any future version of Python-Markdown. + +See <https://Python-Markdown.github.io/extensions/extra> +for documentation. + +Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension + +extensions = [ + 'fenced_code', + 'footnotes', + 'attr_list', + 'def_list', + 'tables', + 'abbr', + 'md_in_html' +] + + +class ExtraExtension(Extension): + """ Add various extensions to Markdown class.""" + + def __init__(self, **kwargs): + """ config is a dumb holder which gets passed to actual ext later. """ + self.config = kwargs + + def extendMarkdown(self, md): + """ Register extension instances. """ + md.registerExtensions(extensions, self.config) + + +def makeExtension(**kwargs): # pragma: no cover + return ExtraExtension(**kwargs) diff --git a/Libs/markdown/extensions/fenced_code.py b/Libs/markdown/extensions/fenced_code.py new file mode 100644 index 0000000..409166a --- /dev/null +++ b/Libs/markdown/extensions/fenced_code.py @@ -0,0 +1,174 @@ +""" +Fenced Code Extension for Python Markdown +========================================= + +This extension adds Fenced Code Blocks to Python-Markdown. + +See <https://Python-Markdown.github.io/extensions/fenced_code_blocks> +for documentation. + +Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). + + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" + + +from textwrap import dedent +from . import Extension +from ..preprocessors import Preprocessor +from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines +from .attr_list import get_attrs, AttrListExtension +from ..util import parseBoolValue +from ..serializers import _escape_attrib_html +import re + + +class FencedCodeExtension(Extension): + def __init__(self, **kwargs): + self.config = { + 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] + } + super().__init__(**kwargs) + + def extendMarkdown(self, md): + """ Add FencedBlockPreprocessor to the Markdown instance. """ + md.registerExtension(self) + + md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25) + + +class FencedBlockPreprocessor(Preprocessor): + FENCED_BLOCK_RE = re.compile( + dedent(r''' + (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence + ((\{(?P<attrs>[^\}\n]*)\})| # (optional {attrs} or + (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang + (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines) + \n # newline (end of opening fence) + (?P<code>.*?)(?<=\n) # the code block + (?P=fence)[ ]*$ # closing fence + '''), + re.MULTILINE | re.DOTALL | re.VERBOSE + ) + + def __init__(self, md, config): + super().__init__(md) + self.config = config + self.checked_for_deps = False + self.codehilite_conf = {} + self.use_attr_list = False + # List of options to convert to bool values + self.bool_options = [ + 'linenums', + 'guess_lang', + 'noclasses', + 'use_pygments' + ] + + def run(self, lines): + """ Match and store Fenced Code Blocks in the HtmlStash. """ + + # Check for dependent extensions + if not self.checked_for_deps: + for ext in self.md.registeredExtensions: + if isinstance(ext, CodeHiliteExtension): + self.codehilite_conf = ext.getConfigs() + if isinstance(ext, AttrListExtension): + self.use_attr_list = True + + self.checked_for_deps = True + + text = "\n".join(lines) + while 1: + m = self.FENCED_BLOCK_RE.search(text) + if m: + lang, id, classes, config = None, '', [], {} + if m.group('attrs'): + id, classes, config = self.handle_attrs(get_attrs(m.group('attrs'))) + if len(classes): + lang = classes.pop(0) + else: + if m.group('lang'): + lang = m.group('lang') + if m.group('hl_lines'): + # Support hl_lines outside of attrs for backward-compatibility + config['hl_lines'] = parse_hl_lines(m.group('hl_lines')) + + # If config is not empty, then the codehighlite extension + # is enabled, so we call it to highlight the code + if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True): + local_config = self.codehilite_conf.copy() + local_config.update(config) + # Combine classes with cssclass. Ensure cssclass is at end + # as pygments appends a suffix under certain circumstances. + # Ignore ID as Pygments does not offer an option to set it. + if classes: + local_config['css_class'] = '{} {}'.format( + ' '.join(classes), + local_config['css_class'] + ) + highliter = CodeHilite( + m.group('code'), + lang=lang, + style=local_config.pop('pygments_style', 'default'), + **local_config + ) + + code = highliter.hilite(shebang=False) + else: + id_attr = lang_attr = class_attr = kv_pairs = '' + if lang: + prefix = self.config.get('lang_prefix', 'language-') + lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"' + if classes: + class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"' + if id: + id_attr = f' id="{_escape_attrib_html(id)}"' + if self.use_attr_list and config and not config.get('use_pygments', False): + # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs + # were defined on the code block, and the `use_pygments` key was not set to True. The + # `use_pygments` key could be either set to False or not defined. It is omitted from output. + kv_pairs = ''.join( + f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments' + ) + code = self._escape(m.group('code')) + code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>' + + placeholder = self.md.htmlStash.store(code) + text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}' + else: + break + return text.split("\n") + + def handle_attrs(self, attrs): + """ Return tuple: (id, [list, of, classes], {configs}) """ + id = '' + classes = [] + configs = {} + for k, v in attrs: + if k == 'id': + id = v + elif k == '.': + classes.append(v) + elif k == 'hl_lines': + configs[k] = parse_hl_lines(v) + elif k in self.bool_options: + configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True) + else: + configs[k] = v + return id, classes, configs + + def _escape(self, txt): + """ basic html escaping """ + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + txt = txt.replace('"', '"') + return txt + + +def makeExtension(**kwargs): # pragma: no cover + return FencedCodeExtension(**kwargs) diff --git a/Libs/markdown/extensions/footnotes.py b/Libs/markdown/extensions/footnotes.py new file mode 100644 index 0000000..1cc7118 --- /dev/null +++ b/Libs/markdown/extensions/footnotes.py @@ -0,0 +1,402 @@ +""" +Footnotes Extension for Python-Markdown +======================================= + +Adds footnote handling to Python-Markdown. + +See <https://Python-Markdown.github.io/extensions/footnotes> +for documentation. + +Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import BlockProcessor +from ..inlinepatterns import InlineProcessor +from ..treeprocessors import Treeprocessor +from ..postprocessors import Postprocessor +from .. import util +from collections import OrderedDict +import re +import copy +import xml.etree.ElementTree as etree + +FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX +NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX +RE_REF_ID = re.compile(r'(fnref)(\d+)') + + +class FootnoteExtension(Extension): + """ Footnote Extension. """ + + def __init__(self, **kwargs): + """ Setup configs. """ + + self.config = { + 'PLACE_MARKER': + ["///Footnotes Go Here///", + "The text string that marks where the footnotes go"], + 'UNIQUE_IDS': + [False, + "Avoid name collisions across " + "multiple calls to reset()."], + "BACKLINK_TEXT": + ["↩", + "The text string that links from the footnote " + "to the reader's place."], + "BACKLINK_TITLE": + ["Jump back to footnote %d in the text", + "The text string used for the title HTML attribute " + "of the backlink. %d will be replaced by the " + "footnote number."], + "SEPARATOR": + [":", + "Footnote separator."] + } + super().__init__(**kwargs) + + # In multiple invocations, emit links that don't get tangled. + self.unique_prefix = 0 + self.found_refs = {} + self.used_refs = set() + + self.reset() + + def extendMarkdown(self, md): + """ Add pieces to Markdown. """ + md.registerExtension(self) + self.parser = md.parser + self.md = md + # Insert a blockprocessor before ReferencePreprocessor + md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17) + + # Insert an inline pattern before ImageReferencePattern + FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah + md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175) + # Insert a tree-processor that would actually add the footnote div + # This must be before all other treeprocessors (i.e., inline and + # codehilite) so they can run on the the contents of the div. + md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50) + + # Insert a tree-processor that will run after inline is done. + # In this tree-processor we want to check our duplicate footnote tracker + # And add additional backrefs to the footnote pointing back to the + # duplicated references. + md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15) + + # Insert a postprocessor after amp_substitute processor + md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25) + + def reset(self): + """ Clear footnotes on reset, and prepare for distinct document. """ + self.footnotes = OrderedDict() + self.unique_prefix += 1 + self.found_refs = {} + self.used_refs = set() + + def unique_ref(self, reference, found=False): + """ Get a unique reference if there are duplicates. """ + if not found: + return reference + + original_ref = reference + while reference in self.used_refs: + ref, rest = reference.split(self.get_separator(), 1) + m = RE_REF_ID.match(ref) + if m: + reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest) + else: + reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest) + + self.used_refs.add(reference) + if original_ref in self.found_refs: + self.found_refs[original_ref] += 1 + else: + self.found_refs[original_ref] = 1 + return reference + + def findFootnotesPlaceholder(self, root): + """ Return ElementTree Element that contains Footnote placeholder. """ + def finder(element): + for child in element: + if child.text: + if child.text.find(self.getConfig("PLACE_MARKER")) > -1: + return child, element, True + if child.tail: + if child.tail.find(self.getConfig("PLACE_MARKER")) > -1: + return child, element, False + child_res = finder(child) + if child_res is not None: + return child_res + return None + + res = finder(root) + return res + + def setFootnote(self, id, text): + """ Store a footnote for later retrieval. """ + self.footnotes[id] = text + + def get_separator(self): + """ Get the footnote separator. """ + return self.getConfig("SEPARATOR") + + def makeFootnoteId(self, id): + """ Return footnote link id. """ + if self.getConfig("UNIQUE_IDS"): + return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id) + else: + return 'fn{}{}'.format(self.get_separator(), id) + + def makeFootnoteRefId(self, id, found=False): + """ Return footnote back-link id. """ + if self.getConfig("UNIQUE_IDS"): + return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found) + else: + return self.unique_ref('fnref{}{}'.format(self.get_separator(), id), found) + + def makeFootnotesDiv(self, root): + """ Return div of footnotes as et Element. """ + + if not list(self.footnotes.keys()): + return None + + div = etree.Element("div") + div.set('class', 'footnote') + etree.SubElement(div, "hr") + ol = etree.SubElement(div, "ol") + surrogate_parent = etree.Element("div") + + for index, id in enumerate(self.footnotes.keys(), start=1): + li = etree.SubElement(ol, "li") + li.set("id", self.makeFootnoteId(id)) + # Parse footnote with surrogate parent as li cannot be used. + # List block handlers have special logic to deal with li. + # When we are done parsing, we will copy everything over to li. + self.parser.parseChunk(surrogate_parent, self.footnotes[id]) + for el in list(surrogate_parent): + li.append(el) + surrogate_parent.remove(el) + backlink = etree.Element("a") + backlink.set("href", "#" + self.makeFootnoteRefId(id)) + backlink.set("class", "footnote-backref") + backlink.set( + "title", + self.getConfig("BACKLINK_TITLE") % (index) + ) + backlink.text = FN_BACKLINK_TEXT + + if len(li): + node = li[-1] + if node.tag == "p": + node.text = node.text + NBSP_PLACEHOLDER + node.append(backlink) + else: + p = etree.SubElement(li, "p") + p.append(backlink) + return div + + +class FootnoteBlockProcessor(BlockProcessor): + """ Find all footnote references and store for later use. """ + + RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE) + + def __init__(self, footnotes): + super().__init__(footnotes.parser) + self.footnotes = footnotes + + def test(self, parent, block): + return True + + def run(self, parent, blocks): + """ Find, set, and remove footnote definitions. """ + block = blocks.pop(0) + m = self.RE.search(block) + if m: + id = m.group(1) + fn_blocks = [m.group(2)] + + # Handle rest of block + therest = block[m.end():].lstrip('\n') + m2 = self.RE.search(therest) + if m2: + # Another footnote exists in the rest of this block. + # Any content before match is continuation of this footnote, which may be lazily indented. + before = therest[:m2.start()].rstrip('\n') + fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n') + # Add back to blocks everything from beginning of match forward for next iteration. + blocks.insert(0, therest[m2.start():]) + else: + # All remaining lines of block are continuation of this footnote, which may be lazily indented. + fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n') + + # Check for child elements in remaining blocks. + fn_blocks.extend(self.detectTabbed(blocks)) + + footnote = "\n\n".join(fn_blocks) + self.footnotes.setFootnote(id, footnote.rstrip()) + + if block[:m.start()].strip(): + # Add any content before match back to blocks as separate block + blocks.insert(0, block[:m.start()].rstrip('\n')) + return True + # No match. Restore block. + blocks.insert(0, block) + return False + + def detectTabbed(self, blocks): + """ Find indented text and remove indent before further proccesing. + + Returns: a list of blocks with indentation removed. + """ + fn_blocks = [] + while blocks: + if blocks[0].startswith(' '*4): + block = blocks.pop(0) + # Check for new footnotes within this block and split at new footnote. + m = self.RE.search(block) + if m: + # Another footnote exists in this block. + # Any content before match is continuation of this footnote, which may be lazily indented. + before = block[:m.start()].rstrip('\n') + fn_blocks.append(self.detab(before)) + # Add back to blocks everything from beginning of match forward for next iteration. + blocks.insert(0, block[m.start():]) + # End of this footnote. + break + else: + # Entire block is part of this footnote. + fn_blocks.append(self.detab(block)) + else: + # End of this footnote. + break + return fn_blocks + + def detab(self, block): + """ Remove one level of indent from a block. + + Preserve lazily indented blocks by only removing indent from indented lines. + """ + lines = block.split('\n') + for i, line in enumerate(lines): + if line.startswith(' '*4): + lines[i] = line[4:] + return '\n'.join(lines) + + +class FootnoteInlineProcessor(InlineProcessor): + """ InlinePattern for footnote markers in a document's body text. """ + + def __init__(self, pattern, footnotes): + super().__init__(pattern) + self.footnotes = footnotes + + def handleMatch(self, m, data): + id = m.group(1) + if id in self.footnotes.footnotes.keys(): + sup = etree.Element("sup") + a = etree.SubElement(sup, "a") + sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True)) + a.set('href', '#' + self.footnotes.makeFootnoteId(id)) + a.set('class', 'footnote-ref') + a.text = str(list(self.footnotes.footnotes.keys()).index(id) + 1) + return sup, m.start(0), m.end(0) + else: + return None, None, None + + +class FootnotePostTreeprocessor(Treeprocessor): + """ Amend footnote div with duplicates. """ + + def __init__(self, footnotes): + self.footnotes = footnotes + + def add_duplicates(self, li, duplicates): + """ Adjust current li and add the duplicates: fnref2, fnref3, etc. """ + for link in li.iter('a'): + # Find the link that needs to be duplicated. + if link.attrib.get('class', '') == 'footnote-backref': + ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1) + # Duplicate link the number of times we need to + # and point the to the appropriate references. + links = [] + for index in range(2, duplicates + 1): + sib_link = copy.deepcopy(link) + sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest) + links.append(sib_link) + self.offset += 1 + # Add all the new duplicate links. + el = list(li)[-1] + for link in links: + el.append(link) + break + + def get_num_duplicates(self, li): + """ Get the number of duplicate refs of the footnote. """ + fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1) + link_id = '{}ref{}{}'.format(fn, self.footnotes.get_separator(), rest) + return self.footnotes.found_refs.get(link_id, 0) + + def handle_duplicates(self, parent): + """ Find duplicate footnotes and format and add the duplicates. """ + for li in list(parent): + # Check number of duplicates footnotes and insert + # additional links if needed. + count = self.get_num_duplicates(li) + if count > 1: + self.add_duplicates(li, count) + + def run(self, root): + """ Crawl the footnote div and add missing duplicate footnotes. """ + self.offset = 0 + for div in root.iter('div'): + if div.attrib.get('class', '') == 'footnote': + # Footnotes should be under the first ordered list under + # the footnote div. So once we find it, quit. + for ol in div.iter('ol'): + self.handle_duplicates(ol) + break + + +class FootnoteTreeprocessor(Treeprocessor): + """ Build and append footnote div to end of document. """ + + def __init__(self, footnotes): + self.footnotes = footnotes + + def run(self, root): + footnotesDiv = self.footnotes.makeFootnotesDiv(root) + if footnotesDiv is not None: + result = self.footnotes.findFootnotesPlaceholder(root) + if result: + child, parent, isText = result + ind = list(parent).index(child) + if isText: + parent.remove(child) + parent.insert(ind, footnotesDiv) + else: + parent.insert(ind + 1, footnotesDiv) + child.tail = None + else: + root.append(footnotesDiv) + + +class FootnotePostprocessor(Postprocessor): + """ Replace placeholders with html entities. """ + def __init__(self, footnotes): + self.footnotes = footnotes + + def run(self, text): + text = text.replace( + FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT") + ) + return text.replace(NBSP_PLACEHOLDER, " ") + + +def makeExtension(**kwargs): # pragma: no cover + """ Return an instance of the FootnoteExtension """ + return FootnoteExtension(**kwargs) diff --git a/Libs/markdown/extensions/legacy_attrs.py b/Libs/markdown/extensions/legacy_attrs.py new file mode 100644 index 0000000..445aba1 --- /dev/null +++ b/Libs/markdown/extensions/legacy_attrs.py @@ -0,0 +1,67 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). + +Legacy Attributes Extension +=========================== + +An extension to Python Markdown which implements legacy attributes. + +Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes` +keyword which was on by default and provided for attributes to be defined for elements +using the format `{@key=value}`. This extension is provided as a replacement for +backward compatibility. New documents should be authored using attr_lists. However, +numerious documents exist which have been using the old attribute format for many +years. This extension can be used to continue to render those documents correctly. +""" + +import re +from markdown.treeprocessors import Treeprocessor, isString +from markdown.extensions import Extension + + +ATTR_RE = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123} + + +class LegacyAttrs(Treeprocessor): + def run(self, doc): + """Find and set values of attributes ({@key=value}). """ + for el in doc.iter(): + alt = el.get('alt', None) + if alt is not None: + el.set('alt', self.handleAttributes(el, alt)) + if el.text and isString(el.text): + el.text = self.handleAttributes(el, el.text) + if el.tail and isString(el.tail): + el.tail = self.handleAttributes(el, el.tail) + + def handleAttributes(self, el, txt): + """ Set attributes and return text without definitions. """ + def attributeCallback(match): + el.set(match.group(1), match.group(2).replace('\n', ' ')) + return ATTR_RE.sub(attributeCallback, txt) + + +class LegacyAttrExtension(Extension): + def extendMarkdown(self, md): + md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15) + + +def makeExtension(**kwargs): # pragma: no cover + return LegacyAttrExtension(**kwargs) diff --git a/Libs/markdown/extensions/legacy_em.py b/Libs/markdown/extensions/legacy_em.py new file mode 100644 index 0000000..360988b --- /dev/null +++ b/Libs/markdown/extensions/legacy_em.py @@ -0,0 +1,49 @@ +''' +Legacy Em Extension for Python-Markdown +======================================= + +This extension provides legacy behavior for _connected_words_. + +Copyright 2015-2018 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +''' + +from . import Extension +from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE +import re + +# _emphasis_ +EMPHASIS_RE = r'(_)([^_]+)\1' + +# __strong__ +STRONG_RE = r'(_{2})(.+?)\1' + +# __strong_em___ +STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}' + + +class LegacyUnderscoreProcessor(UnderscoreProcessor): + """Emphasis processor for handling strong and em matches inside underscores.""" + + PATTERNS = [ + EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), + EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), + EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), + EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), + EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') + ] + + +class LegacyEmExtension(Extension): + """ Add legacy_em extension to Markdown class.""" + + def extendMarkdown(self, md): + """ Modify inline patterns. """ + md.inlinePatterns.register(LegacyUnderscoreProcessor(r'_'), 'em_strong2', 50) + + +def makeExtension(**kwargs): # pragma: no cover + """ Return an instance of the LegacyEmExtension """ + return LegacyEmExtension(**kwargs) diff --git a/Libs/markdown/extensions/md_in_html.py b/Libs/markdown/extensions/md_in_html.py new file mode 100644 index 0000000..ec7dcba --- /dev/null +++ b/Libs/markdown/extensions/md_in_html.py @@ -0,0 +1,364 @@ +""" +Python-Markdown Markdown in HTML Extension +=============================== + +An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s +parsing of Markdown syntax in raw HTML. + +See <https://Python-Markdown.github.io/extensions/raw_html> +for documentation. + +Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import BlockProcessor +from ..preprocessors import Preprocessor +from ..postprocessors import RawHtmlPostprocessor +from .. import util +from ..htmlparser import HTMLExtractor, blank_line_re +import xml.etree.ElementTree as etree + + +class HTMLExtractorExtra(HTMLExtractor): + """ + Override HTMLExtractor and create etree Elements for any elements which should have content parsed as Markdown. + """ + + def __init__(self, md, *args, **kwargs): + # All block-level tags. + self.block_level_tags = set(md.block_level_elements.copy()) + # Block-level tags in which the content only gets span level parsing + self.span_tags = set( + ['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'summary', 'td', 'th'] + ) + # Block-level tags which never get their content parsed. + self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea']) + + super().__init__(md, *args, **kwargs) + + # Block-level tags in which the content gets parsed as blocks + self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags) + self.span_and_blocks_tags = self.block_tags | self.span_tags + + def reset(self): + """Reset this instance. Loses all unprocessed data.""" + self.mdstack = [] # When markdown=1, stack contains a list of tags + self.treebuilder = etree.TreeBuilder() + self.mdstate = [] # one of 'block', 'span', 'off', or None + super().reset() + + def close(self): + """Handle any buffered data.""" + super().close() + # Handle any unclosed tags. + if self.mdstack: + # Close the outermost parent. handle_endtag will close all unclosed children. + self.handle_endtag(self.mdstack[0]) + + def get_element(self): + """ Return element from treebuilder and reset treebuilder for later use. """ + element = self.treebuilder.close() + self.treebuilder = etree.TreeBuilder() + return element + + def get_state(self, tag, attrs): + """ Return state from tag and `markdown` attr. One of 'block', 'span', or 'off'. """ + md_attr = attrs.get('markdown', '0') + if md_attr == 'markdown': + # `<tag markdown>` is the same as `<tag markdown='1'>`. + md_attr = '1' + parent_state = self.mdstate[-1] if self.mdstate else None + if parent_state == 'off' or (parent_state == 'span' and md_attr != '0'): + # Only use the parent state if it is more restrictive than the markdown attribute. + md_attr = parent_state + if ((md_attr == '1' and tag in self.block_tags) or + (md_attr == 'block' and tag in self.span_and_blocks_tags)): + return 'block' + elif ((md_attr == '1' and tag in self.span_tags) or + (md_attr == 'span' and tag in self.span_and_blocks_tags)): + return 'span' + elif tag in self.block_level_tags: + return 'off' + else: # pragma: no cover + return None + + def handle_starttag(self, tag, attrs): + # Handle tags that should always be empty and do not specify a closing tag + if tag in self.empty_tags and (self.at_line_start() or self.intail): + attrs = {key: value if value is not None else key for key, value in attrs} + if "markdown" in attrs: + attrs.pop('markdown') + element = etree.Element(tag, attrs) + data = etree.tostring(element, encoding='unicode', method='html') + else: + data = self.get_starttag_text() + self.handle_empty_tag(data, True) + return + + if tag in self.block_level_tags and (self.at_line_start() or self.intail): + # Valueless attr (ex: `<tag checked>`) results in `[('checked', None)]`. + # Convert to `{'checked': 'checked'}`. + attrs = {key: value if value is not None else key for key, value in attrs} + state = self.get_state(tag, attrs) + if self.inraw or (state in [None, 'off'] and not self.mdstack): + # fall back to default behavior + attrs.pop('markdown', None) + super().handle_starttag(tag, attrs) + else: + if 'p' in self.mdstack and tag in self.block_level_tags: + # Close unclosed 'p' tag + self.handle_endtag('p') + self.mdstate.append(state) + self.mdstack.append(tag) + attrs['markdown'] = state + self.treebuilder.start(tag, attrs) + else: + # Span level tag + if self.inraw: + super().handle_starttag(tag, attrs) + else: + text = self.get_starttag_text() + if self.mdstate and self.mdstate[-1] == "off": + self.handle_data(self.md.htmlStash.store(text)) + else: + self.handle_data(text) + if tag in self.CDATA_CONTENT_ELEMENTS: + # This is presumably a standalone tag in a code span (see #1036). + self.clear_cdata_mode() + + def handle_endtag(self, tag): + if tag in self.block_level_tags: + if self.inraw: + super().handle_endtag(tag) + elif tag in self.mdstack: + # Close element and any unclosed children + while self.mdstack: + item = self.mdstack.pop() + self.mdstate.pop() + self.treebuilder.end(item) + if item == tag: + break + if not self.mdstack: + # Last item in stack is closed. Stash it + element = self.get_element() + # Get last entry to see if it ends in newlines + # If it is an element, assume there is no newlines + item = self.cleandoc[-1] if self.cleandoc else '' + # If we only have one newline before block element, add another + if not item.endswith('\n\n') and item.endswith('\n'): + self.cleandoc.append('\n') + self.cleandoc.append(self.md.htmlStash.store(element)) + self.cleandoc.append('\n\n') + self.state = [] + # Check if element has a tail + if not blank_line_re.match( + self.rawdata[self.line_offset + self.offset + len(self.get_endtag_text(tag)):]): + # More content exists after endtag. + self.intail = True + else: + # Treat orphan closing tag as a span level tag. + text = self.get_endtag_text(tag) + if self.mdstate and self.mdstate[-1] == "off": + self.handle_data(self.md.htmlStash.store(text)) + else: + self.handle_data(text) + else: + # Span level tag + if self.inraw: + super().handle_endtag(tag) + else: + text = self.get_endtag_text(tag) + if self.mdstate and self.mdstate[-1] == "off": + self.handle_data(self.md.htmlStash.store(text)) + else: + self.handle_data(text) + + def handle_startendtag(self, tag, attrs): + if tag in self.empty_tags: + attrs = {key: value if value is not None else key for key, value in attrs} + if "markdown" in attrs: + attrs.pop('markdown') + element = etree.Element(tag, attrs) + data = etree.tostring(element, encoding='unicode', method='html') + else: + data = self.get_starttag_text() + else: + data = self.get_starttag_text() + self.handle_empty_tag(data, is_block=self.md.is_block_level(tag)) + + def handle_data(self, data): + if self.intail and '\n' in data: + self.intail = False + if self.inraw or not self.mdstack: + super().handle_data(data) + else: + self.treebuilder.data(data) + + def handle_empty_tag(self, data, is_block): + if self.inraw or not self.mdstack: + super().handle_empty_tag(data, is_block) + else: + if self.at_line_start() and is_block: + self.handle_data('\n' + self.md.htmlStash.store(data) + '\n\n') + else: + self.handle_data(self.md.htmlStash.store(data)) + + def parse_pi(self, i): + if self.at_line_start() or self.intail or self.mdstack: + # The same override exists in HTMLExtractor without the check + # for mdstack. Therefore, use HTMLExtractor's parent instead. + return super(HTMLExtractor, self).parse_pi(i) + # This is not the beginning of a raw block so treat as plain data + # and avoid consuming any tags which may follow (see #1066). + self.handle_data('<?') + return i + 2 + + def parse_html_declaration(self, i): + if self.at_line_start() or self.intail or self.mdstack: + # The same override exists in HTMLExtractor without the check + # for mdstack. Therefore, use HTMLExtractor's parent instead. + return super(HTMLExtractor, self).parse_html_declaration(i) + # This is not the beginning of a raw block so treat as plain data + # and avoid consuming any tags which may follow (see #1066). + self.handle_data('<!') + return i + 2 + + +class HtmlBlockPreprocessor(Preprocessor): + """Remove html blocks from the text and store them for later retrieval.""" + + def run(self, lines): + source = '\n'.join(lines) + parser = HTMLExtractorExtra(self.md) + parser.feed(source) + parser.close() + return ''.join(parser.cleandoc).split('\n') + + +class MarkdownInHtmlProcessor(BlockProcessor): + """Process Markdown Inside HTML Blocks which have been stored in the HtmlStash.""" + + def test(self, parent, block): + # ALways return True. `run` will return `False` it not a valid match. + return True + + def parse_element_content(self, element): + """ + Recursively parse the text content of an etree Element as Markdown. + + Any block level elements generated from the Markdown will be inserted as children of the element in place + of the text content. All `markdown` attributes are removed. For any elements in which Markdown parsing has + been disabled, the text content of it and its chidlren are wrapped in an `AtomicString`. + """ + + md_attr = element.attrib.pop('markdown', 'off') + + if md_attr == 'block': + # Parse content as block level + # The order in which the different parts are parsed (text, children, tails) is important here as the + # order of elements needs to be preserved. We can't be inserting items at a later point in the current + # iteration as we don't want to do raw processing on elements created from parsing Markdown text (for + # example). Therefore, the order of operations is children, tails, text. + + # Recursively parse existing children from raw HTML + for child in list(element): + self.parse_element_content(child) + + # Parse Markdown text in tail of children. Do this separate to avoid raw HTML parsing. + # Save the position of each item to be inserted later in reverse. + tails = [] + for pos, child in enumerate(element): + if child.tail: + block = child.tail.rstrip('\n') + child.tail = '' + # Use a dummy placeholder element. + dummy = etree.Element('div') + self.parser.parseBlocks(dummy, block.split('\n\n')) + children = list(dummy) + children.reverse() + tails.append((pos + 1, children)) + + # Insert the elements created from the tails in reverse. + tails.reverse() + for pos, tail in tails: + for item in tail: + element.insert(pos, item) + + # Parse Markdown text content. Do this last to avoid raw HTML parsing. + if element.text: + block = element.text.rstrip('\n') + element.text = '' + # Use a dummy placeholder element as the content needs to get inserted before existing children. + dummy = etree.Element('div') + self.parser.parseBlocks(dummy, block.split('\n\n')) + children = list(dummy) + children.reverse() + for child in children: + element.insert(0, child) + + elif md_attr == 'span': + # Span level parsing will be handled by inlineprocessors. + # Walk children here to remove any `markdown` attributes. + for child in list(element): + self.parse_element_content(child) + + else: + # Disable inline parsing for everything else + if element.text is None: + element.text = '' + element.text = util.AtomicString(element.text) + for child in list(element): + self.parse_element_content(child) + if child.tail: + child.tail = util.AtomicString(child.tail) + + def run(self, parent, blocks): + m = util.HTML_PLACEHOLDER_RE.match(blocks[0]) + if m: + index = int(m.group(1)) + element = self.parser.md.htmlStash.rawHtmlBlocks[index] + if isinstance(element, etree.Element): + # We have a matched element. Process it. + blocks.pop(0) + self.parse_element_content(element) + parent.append(element) + # Cleanup stash. Replace element with empty string to avoid confusing postprocessor. + self.parser.md.htmlStash.rawHtmlBlocks.pop(index) + self.parser.md.htmlStash.rawHtmlBlocks.insert(index, '') + # Confirm the match to the blockparser. + return True + # No match found. + return False + + +class MarkdownInHTMLPostprocessor(RawHtmlPostprocessor): + def stash_to_string(self, text): + """ Override default to handle any etree elements still in the stash. """ + if isinstance(text, etree.Element): + return self.md.serializer(text) + else: + return str(text) + + +class MarkdownInHtmlExtension(Extension): + """Add Markdown parsing in HTML to Markdown class.""" + + def extendMarkdown(self, md): + """ Register extension instances. """ + + # Replace raw HTML preprocessor + md.preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20) + # Add blockprocessor which handles the placeholders for etree elements + md.parser.blockprocessors.register( + MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105 + ) + # Replace raw HTML postprocessor + md.postprocessors.register(MarkdownInHTMLPostprocessor(md), 'raw_html', 30) + + +def makeExtension(**kwargs): # pragma: no cover + return MarkdownInHtmlExtension(**kwargs) diff --git a/Libs/markdown/extensions/meta.py b/Libs/markdown/extensions/meta.py new file mode 100644 index 0000000..10dee11 --- /dev/null +++ b/Libs/markdown/extensions/meta.py @@ -0,0 +1,79 @@ +""" +Meta Data Extension for Python-Markdown +======================================= + +This extension adds Meta Data handling to markdown. + +See <https://Python-Markdown.github.io/extensions/meta_data> +for documentation. + +Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com). + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..preprocessors import Preprocessor +import re +import logging + +log = logging.getLogger('MARKDOWN') + +# Global Vars +META_RE = re.compile(r'^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)') +META_MORE_RE = re.compile(r'^[ ]{4,}(?P<value>.*)') +BEGIN_RE = re.compile(r'^-{3}(\s.*)?') +END_RE = re.compile(r'^(-{3}|\.{3})(\s.*)?') + + +class MetaExtension (Extension): + """ Meta-Data extension for Python-Markdown. """ + + def extendMarkdown(self, md): + """ Add MetaPreprocessor to Markdown instance. """ + md.registerExtension(self) + self.md = md + md.preprocessors.register(MetaPreprocessor(md), 'meta', 27) + + def reset(self): + self.md.Meta = {} + + +class MetaPreprocessor(Preprocessor): + """ Get Meta-Data. """ + + def run(self, lines): + """ Parse Meta-Data and store in Markdown.Meta. """ + meta = {} + key = None + if lines and BEGIN_RE.match(lines[0]): + lines.pop(0) + while lines: + line = lines.pop(0) + m1 = META_RE.match(line) + if line.strip() == '' or END_RE.match(line): + break # blank line or end of YAML header - done + if m1: + key = m1.group('key').lower().strip() + value = m1.group('value').strip() + try: + meta[key].append(value) + except KeyError: + meta[key] = [value] + else: + m2 = META_MORE_RE.match(line) + if m2 and key: + # Add another line to existing key + meta[key].append(m2.group('value').strip()) + else: + lines.insert(0, line) + break # no meta data - done + self.md.Meta = meta + return lines + + +def makeExtension(**kwargs): # pragma: no cover + return MetaExtension(**kwargs) diff --git a/Libs/markdown/extensions/nl2br.py b/Libs/markdown/extensions/nl2br.py new file mode 100644 index 0000000..6c7491b --- /dev/null +++ b/Libs/markdown/extensions/nl2br.py @@ -0,0 +1,33 @@ +""" +NL2BR Extension +=============== + +A Python-Markdown extension to treat newlines as hard breaks; like +GitHub-flavored Markdown does. + +See <https://Python-Markdown.github.io/extensions/nl2br> +for documentation. + +Oringinal code Copyright 2011 [Brian Neal](https://deathofagremmie.com/) + +All changes Copyright 2011-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..inlinepatterns import SubstituteTagInlineProcessor + +BR_RE = r'\n' + + +class Nl2BrExtension(Extension): + + def extendMarkdown(self, md): + br_tag = SubstituteTagInlineProcessor(BR_RE, 'br') + md.inlinePatterns.register(br_tag, 'nl', 5) + + +def makeExtension(**kwargs): # pragma: no cover + return Nl2BrExtension(**kwargs) diff --git a/Libs/markdown/extensions/sane_lists.py b/Libs/markdown/extensions/sane_lists.py new file mode 100644 index 0000000..e27eb18 --- /dev/null +++ b/Libs/markdown/extensions/sane_lists.py @@ -0,0 +1,54 @@ +""" +Sane List Extension for Python-Markdown +======================================= + +Modify the behavior of Lists in Python-Markdown to act in a sane manor. + +See <https://Python-Markdown.github.io/extensions/sane_lists> +for documentation. + +Original code Copyright 2011 [Waylan Limberg](http://achinghead.com) + +All changes Copyright 2011-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import OListProcessor, UListProcessor +import re + + +class SaneOListProcessor(OListProcessor): + + SIBLING_TAGS = ['ol'] + LAZY_OL = False + + def __init__(self, parser): + super().__init__(parser) + self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.))[ ]+(.*)' % + (self.tab_length - 1)) + + +class SaneUListProcessor(UListProcessor): + + SIBLING_TAGS = ['ul'] + + def __init__(self, parser): + super().__init__(parser) + self.CHILD_RE = re.compile(r'^[ ]{0,%d}(([*+-]))[ ]+(.*)' % + (self.tab_length - 1)) + + +class SaneListExtension(Extension): + """ Add sane lists to Markdown. """ + + def extendMarkdown(self, md): + """ Override existing Processors. """ + md.parser.blockprocessors.register(SaneOListProcessor(md.parser), 'olist', 40) + md.parser.blockprocessors.register(SaneUListProcessor(md.parser), 'ulist', 30) + + +def makeExtension(**kwargs): # pragma: no cover + return SaneListExtension(**kwargs) diff --git a/Libs/markdown/extensions/smarty.py b/Libs/markdown/extensions/smarty.py new file mode 100644 index 0000000..894805f --- /dev/null +++ b/Libs/markdown/extensions/smarty.py @@ -0,0 +1,263 @@ +''' +Smarty extension for Python-Markdown +==================================== + +Adds conversion of ASCII dashes, quotes and ellipses to their HTML +entity equivalents. + +See <https://Python-Markdown.github.io/extensions/smarty> +for documentation. + +Author: 2013, Dmitry Shachnev <mitya57@gmail.com> + +All changes Copyright 2013-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +SmartyPants license: + + Copyright (c) 2003 John Gruber <https://daringfireball.net/> + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name "SmartyPants" nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + + This software is provided by the copyright holders and contributors "as + is" and any express or implied warranties, including, but not limited + to, the implied warranties of merchantability and fitness for a + particular purpose are disclaimed. In no event shall the copyright + owner or contributors be liable for any direct, indirect, incidental, + special, exemplary, or consequential damages (including, but not + limited to, procurement of substitute goods or services; loss of use, + data, or profits; or business interruption) however caused and on any + theory of liability, whether in contract, strict liability, or tort + (including negligence or otherwise) arising in any way out of the use + of this software, even if advised of the possibility of such damage. + + +smartypants.py license: + + smartypants.py is a derivative work of SmartyPants. + Copyright (c) 2004, 2007 Chad Miller <http://web.chad.org/> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + This software is provided by the copyright holders and contributors "as + is" and any express or implied warranties, including, but not limited + to, the implied warranties of merchantability and fitness for a + particular purpose are disclaimed. In no event shall the copyright + owner or contributors be liable for any direct, indirect, incidental, + special, exemplary, or consequential damages (including, but not + limited to, procurement of substitute goods or services; loss of use, + data, or profits; or business interruption) however caused and on any + theory of liability, whether in contract, strict liability, or tort + (including negligence or otherwise) arising in any way out of the use + of this software, even if advised of the possibility of such damage. + +''' + + +from . import Extension +from ..inlinepatterns import HtmlInlineProcessor, HTML_RE +from ..treeprocessors import InlineProcessor +from ..util import Registry, deprecated + + +# Constants for quote education. +punctClass = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]""" +endOfWordClass = r"[\s.,;:!?)]" +closeClass = r"[^\ \t\r\n\[\{\(\-\u0002\u0003]" + +openingQuotesBase = ( + r'(\s' # a whitespace char + r'| ' # or a non-breaking space entity + r'|--' # or dashes + r'|–|—' # or unicode + r'|&[mn]dash;' # or named dash entities + r'|–|—' # or decimal entities + r')' +) + +substitutions = { + 'mdash': '—', + 'ndash': '–', + 'ellipsis': '…', + 'left-angle-quote': '«', + 'right-angle-quote': '»', + 'left-single-quote': '‘', + 'right-single-quote': '’', + 'left-double-quote': '“', + 'right-double-quote': '”', +} + + +# Special case if the very first character is a quote +# followed by punctuation at a non-word-break. Close the quotes by brute force: +singleQuoteStartRe = r"^'(?=%s\B)" % punctClass +doubleQuoteStartRe = r'^"(?=%s\B)' % punctClass + +# Special case for double sets of quotes, e.g.: +# <p>He said, "'Quoted' words in a larger quote."</p> +doubleQuoteSetsRe = r""""'(?=\w)""" +singleQuoteSetsRe = r"""'"(?=\w)""" + +# Special case for decade abbreviations (the '80s): +decadeAbbrRe = r"(?<!\w)'(?=\d{2}s)" + +# Get most opening double quotes: +openingDoubleQuotesRegex = r'%s"(?=\w)' % openingQuotesBase + +# Double closing quotes: +closingDoubleQuotesRegex = r'"(?=\s)' +closingDoubleQuotesRegex2 = '(?<=%s)"' % closeClass + +# Get most opening single quotes: +openingSingleQuotesRegex = r"%s'(?=\w)" % openingQuotesBase + +# Single closing quotes: +closingSingleQuotesRegex = r"(?<=%s)'(?!\s|s\b|\d)" % closeClass +closingSingleQuotesRegex2 = r"(?<=%s)'(\s|s\b)" % closeClass + +# All remaining quotes should be opening ones +remainingSingleQuotesRegex = r"'" +remainingDoubleQuotesRegex = r'"' + +HTML_STRICT_RE = HTML_RE + r'(?!\>)' + + +class SubstituteTextPattern(HtmlInlineProcessor): + def __init__(self, pattern, replace, md): + """ Replaces matches with some text. """ + HtmlInlineProcessor.__init__(self, pattern) + self.replace = replace + self.md = md + + @property + @deprecated("Use 'md' instead.") + def markdown(self): + # TODO: remove this later + return self.md + + def handleMatch(self, m, data): + result = '' + for part in self.replace: + if isinstance(part, int): + result += m.group(part) + else: + result += self.md.htmlStash.store(part) + return result, m.start(0), m.end(0) + + +class SmartyExtension(Extension): + def __init__(self, **kwargs): + self.config = { + 'smart_quotes': [True, 'Educate quotes'], + 'smart_angled_quotes': [False, 'Educate angled quotes'], + 'smart_dashes': [True, 'Educate dashes'], + 'smart_ellipses': [True, 'Educate ellipses'], + 'substitutions': [{}, 'Overwrite default substitutions'], + } + super().__init__(**kwargs) + self.substitutions = dict(substitutions) + self.substitutions.update(self.getConfig('substitutions', default={})) + + def _addPatterns(self, md, patterns, serie, priority): + for ind, pattern in enumerate(patterns): + pattern += (md,) + pattern = SubstituteTextPattern(*pattern) + name = 'smarty-%s-%d' % (serie, ind) + self.inlinePatterns.register(pattern, name, priority-ind) + + def educateDashes(self, md): + emDashesPattern = SubstituteTextPattern( + r'(?<!-)---(?!-)', (self.substitutions['mdash'],), md + ) + enDashesPattern = SubstituteTextPattern( + r'(?<!-)--(?!-)', (self.substitutions['ndash'],), md + ) + self.inlinePatterns.register(emDashesPattern, 'smarty-em-dashes', 50) + self.inlinePatterns.register(enDashesPattern, 'smarty-en-dashes', 45) + + def educateEllipses(self, md): + ellipsesPattern = SubstituteTextPattern( + r'(?<!\.)\.{3}(?!\.)', (self.substitutions['ellipsis'],), md + ) + self.inlinePatterns.register(ellipsesPattern, 'smarty-ellipses', 10) + + def educateAngledQuotes(self, md): + leftAngledQuotePattern = SubstituteTextPattern( + r'\<\<', (self.substitutions['left-angle-quote'],), md + ) + rightAngledQuotePattern = SubstituteTextPattern( + r'\>\>', (self.substitutions['right-angle-quote'],), md + ) + self.inlinePatterns.register(leftAngledQuotePattern, 'smarty-left-angle-quotes', 40) + self.inlinePatterns.register(rightAngledQuotePattern, 'smarty-right-angle-quotes', 35) + + def educateQuotes(self, md): + lsquo = self.substitutions['left-single-quote'] + rsquo = self.substitutions['right-single-quote'] + ldquo = self.substitutions['left-double-quote'] + rdquo = self.substitutions['right-double-quote'] + patterns = ( + (singleQuoteStartRe, (rsquo,)), + (doubleQuoteStartRe, (rdquo,)), + (doubleQuoteSetsRe, (ldquo + lsquo,)), + (singleQuoteSetsRe, (lsquo + ldquo,)), + (decadeAbbrRe, (rsquo,)), + (openingSingleQuotesRegex, (1, lsquo)), + (closingSingleQuotesRegex, (rsquo,)), + (closingSingleQuotesRegex2, (rsquo, 1)), + (remainingSingleQuotesRegex, (lsquo,)), + (openingDoubleQuotesRegex, (1, ldquo)), + (closingDoubleQuotesRegex, (rdquo,)), + (closingDoubleQuotesRegex2, (rdquo,)), + (remainingDoubleQuotesRegex, (ldquo,)) + ) + self._addPatterns(md, patterns, 'quotes', 30) + + def extendMarkdown(self, md): + configs = self.getConfigs() + self.inlinePatterns = Registry() + if configs['smart_ellipses']: + self.educateEllipses(md) + if configs['smart_quotes']: + self.educateQuotes(md) + if configs['smart_angled_quotes']: + self.educateAngledQuotes(md) + # Override HTML_RE from inlinepatterns.py so that it does not + # process tags with duplicate closing quotes. + md.inlinePatterns.register(HtmlInlineProcessor(HTML_STRICT_RE, md), 'html', 90) + if configs['smart_dashes']: + self.educateDashes(md) + inlineProcessor = InlineProcessor(md) + inlineProcessor.inlinePatterns = self.inlinePatterns + md.treeprocessors.register(inlineProcessor, 'smarty', 2) + md.ESCAPED_CHARS.extend(['"', "'"]) + + +def makeExtension(**kwargs): # pragma: no cover + return SmartyExtension(**kwargs) diff --git a/Libs/markdown/extensions/tables.py b/Libs/markdown/extensions/tables.py new file mode 100644 index 0000000..0a9d084 --- /dev/null +++ b/Libs/markdown/extensions/tables.py @@ -0,0 +1,223 @@ +""" +Tables Extension for Python-Markdown +==================================== + +Added parsing of tables to Python-Markdown. + +See <https://Python-Markdown.github.io/extensions/tables> +for documentation. + +Original code Copyright 2009 [Waylan Limberg](http://achinghead.com) + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..blockprocessors import BlockProcessor +import xml.etree.ElementTree as etree +import re +PIPE_NONE = 0 +PIPE_LEFT = 1 +PIPE_RIGHT = 2 + + +class TableProcessor(BlockProcessor): + """ Process Tables. """ + + RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))') + RE_END_BORDER = re.compile(r'(?<!\\)(?:\\\\)*\|$') + + def __init__(self, parser): + self.border = False + self.separator = '' + super().__init__(parser) + + def test(self, parent, block): + """ + Ensure first two rows (column header and separator row) are valid table rows. + + Keep border check and separator row do avoid repeating the work. + """ + is_table = False + rows = [row.strip(' ') for row in block.split('\n')] + if len(rows) > 1: + header0 = rows[0] + self.border = PIPE_NONE + if header0.startswith('|'): + self.border |= PIPE_LEFT + if self.RE_END_BORDER.search(header0) is not None: + self.border |= PIPE_RIGHT + row = self._split_row(header0) + row0_len = len(row) + is_table = row0_len > 1 + + # Each row in a single column table needs at least one pipe. + if not is_table and row0_len == 1 and self.border: + for index in range(1, len(rows)): + is_table = rows[index].startswith('|') + if not is_table: + is_table = self.RE_END_BORDER.search(rows[index]) is not None + if not is_table: + break + + if is_table: + row = self._split_row(rows[1]) + is_table = (len(row) == row0_len) and set(''.join(row)) <= set('|:- ') + if is_table: + self.separator = row + + return is_table + + def run(self, parent, blocks): + """ Parse a table block and build table. """ + block = blocks.pop(0).split('\n') + header = block[0].strip(' ') + rows = [] if len(block) < 3 else block[2:] + + # Get alignment of columns + align = [] + for c in self.separator: + c = c.strip(' ') + if c.startswith(':') and c.endswith(':'): + align.append('center') + elif c.startswith(':'): + align.append('left') + elif c.endswith(':'): + align.append('right') + else: + align.append(None) + + # Build table + table = etree.SubElement(parent, 'table') + thead = etree.SubElement(table, 'thead') + self._build_row(header, thead, align) + tbody = etree.SubElement(table, 'tbody') + if len(rows) == 0: + # Handle empty table + self._build_empty_row(tbody, align) + else: + for row in rows: + self._build_row(row.strip(' '), tbody, align) + + def _build_empty_row(self, parent, align): + """Build an empty row.""" + tr = etree.SubElement(parent, 'tr') + count = len(align) + while count: + etree.SubElement(tr, 'td') + count -= 1 + + def _build_row(self, row, parent, align): + """ Given a row of text, build table cells. """ + tr = etree.SubElement(parent, 'tr') + tag = 'td' + if parent.tag == 'thead': + tag = 'th' + cells = self._split_row(row) + # We use align here rather than cells to ensure every row + # contains the same number of columns. + for i, a in enumerate(align): + c = etree.SubElement(tr, tag) + try: + c.text = cells[i].strip(' ') + except IndexError: # pragma: no cover + c.text = "" + if a: + c.set('align', a) + + def _split_row(self, row): + """ split a row of text into list of cells. """ + if self.border: + if row.startswith('|'): + row = row[1:] + row = self.RE_END_BORDER.sub('', row) + return self._split(row) + + def _split(self, row): + """ split a row of text with some code into a list of cells. """ + elements = [] + pipes = [] + tics = [] + tic_points = [] + tic_region = [] + good_pipes = [] + + # Parse row + # Throw out \\, and \| + for m in self.RE_CODE_PIPES.finditer(row): + # Store ` data (len, start_pos, end_pos) + if m.group(2): + # \`+ + # Store length of each tic group: subtract \ + tics.append(len(m.group(2)) - 1) + # Store start of group, end of group, and escape length + tic_points.append((m.start(2), m.end(2) - 1, 1)) + elif m.group(3): + # `+ + # Store length of each tic group + tics.append(len(m.group(3))) + # Store start of group, end of group, and escape length + tic_points.append((m.start(3), m.end(3) - 1, 0)) + # Store pipe location + elif m.group(5): + pipes.append(m.start(5)) + + # Pair up tics according to size if possible + # Subtract the escape length *only* from the opening. + # Walk through tic list and see if tic has a close. + # Store the tic region (start of region, end of region). + pos = 0 + tic_len = len(tics) + while pos < tic_len: + try: + tic_size = tics[pos] - tic_points[pos][2] + if tic_size == 0: + raise ValueError + index = tics[pos + 1:].index(tic_size) + 1 + tic_region.append((tic_points[pos][0], tic_points[pos + index][1])) + pos += index + 1 + except ValueError: + pos += 1 + + # Resolve pipes. Check if they are within a tic pair region. + # Walk through pipes comparing them to each region. + # - If pipe position is less that a region, it isn't in a region + # - If it is within a region, we don't want it, so throw it out + # - If we didn't throw it out, it must be a table pipe + for pipe in pipes: + throw_out = False + for region in tic_region: + if pipe < region[0]: + # Pipe is not in a region + break + elif region[0] <= pipe <= region[1]: + # Pipe is within a code region. Throw it out. + throw_out = True + break + if not throw_out: + good_pipes.append(pipe) + + # Split row according to table delimiters. + pos = 0 + for pipe in good_pipes: + elements.append(row[pos:pipe]) + pos = pipe + 1 + elements.append(row[pos:]) + return elements + + +class TableExtension(Extension): + """ Add tables to Markdown. """ + + def extendMarkdown(self, md): + """ Add an instance of TableProcessor to BlockParser. """ + if '|' not in md.ESCAPED_CHARS: + md.ESCAPED_CHARS.append('|') + md.parser.blockprocessors.register(TableProcessor(md.parser), 'table', 75) + + +def makeExtension(**kwargs): # pragma: no cover + return TableExtension(**kwargs) diff --git a/Libs/markdown/extensions/toc.py b/Libs/markdown/extensions/toc.py new file mode 100644 index 0000000..57d2e3b --- /dev/null +++ b/Libs/markdown/extensions/toc.py @@ -0,0 +1,380 @@ +""" +Table of Contents Extension for Python-Markdown +=============================================== + +See <https://Python-Markdown.github.io/extensions/toc> +for documentation. + +Oringinal code Copyright 2008 [Jack Miller](https://codezen.org/) + +All changes Copyright 2008-2014 The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from . import Extension +from ..treeprocessors import Treeprocessor +from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString +from ..postprocessors import UnescapePostprocessor +import re +import html +import unicodedata +import xml.etree.ElementTree as etree + + +def slugify(value, separator, unicode=False): + """ Slugify a string, to make it URL friendly. """ + if not unicode: + # Replace Extended Latin characters with ASCII, i.e. žlutý → zluty + value = unicodedata.normalize('NFKD', value) + value = value.encode('ascii', 'ignore').decode('ascii') + value = re.sub(r'[^\w\s-]', '', value).strip().lower() + return re.sub(r'[{}\s]+'.format(separator), separator, value) + + +def slugify_unicode(value, separator): + """ Slugify a string, to make it URL friendly while preserving Unicode characters. """ + return slugify(value, separator, unicode=True) + + +IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$') + + +def unique(id, ids): + """ Ensure id is unique in set of ids. Append '_1', '_2'... if not """ + while id in ids or not id: + m = IDCOUNT_RE.match(id) + if m: + id = '%s_%d' % (m.group(1), int(m.group(2))+1) + else: + id = '%s_%d' % (id, 1) + ids.add(id) + return id + + +def get_name(el): + """Get title name.""" + + text = [] + for c in el.itertext(): + if isinstance(c, AtomicString): + text.append(html.unescape(c)) + else: + text.append(c) + return ''.join(text).strip() + + +def stashedHTML2text(text, md, strip_entities=True): + """ Extract raw HTML from stash, reduce to plain text and swap with placeholder. """ + def _html_sub(m): + """ Substitute raw html with plain text. """ + try: + raw = md.htmlStash.rawHtmlBlocks[int(m.group(1))] + except (IndexError, TypeError): # pragma: no cover + return m.group(0) + # Strip out tags and/or entities - leaving text + res = re.sub(r'(<[^>]+>)', '', raw) + if strip_entities: + res = re.sub(r'(&[\#a-zA-Z0-9]+;)', '', res) + return res + + return HTML_PLACEHOLDER_RE.sub(_html_sub, text) + + +def unescape(text): + """ Unescape escaped text. """ + c = UnescapePostprocessor() + return c.run(text) + + +def nest_toc_tokens(toc_list): + """Given an unsorted list with errors and skips, return a nested one. + [{'level': 1}, {'level': 2}] + => + [{'level': 1, 'children': [{'level': 2, 'children': []}]}] + + A wrong list is also converted: + [{'level': 2}, {'level': 1}] + => + [{'level': 2, 'children': []}, {'level': 1, 'children': []}] + """ + + ordered_list = [] + if len(toc_list): + # Initialize everything by processing the first entry + last = toc_list.pop(0) + last['children'] = [] + levels = [last['level']] + ordered_list.append(last) + parents = [] + + # Walk the rest nesting the entries properly + while toc_list: + t = toc_list.pop(0) + current_level = t['level'] + t['children'] = [] + + # Reduce depth if current level < last item's level + if current_level < levels[-1]: + # Pop last level since we know we are less than it + levels.pop() + + # Pop parents and levels we are less than or equal to + to_pop = 0 + for p in reversed(parents): + if current_level <= p['level']: + to_pop += 1 + else: # pragma: no cover + break + if to_pop: + levels = levels[:-to_pop] + parents = parents[:-to_pop] + + # Note current level as last + levels.append(current_level) + + # Level is the same, so append to + # the current parent (if available) + if current_level == levels[-1]: + (parents[-1]['children'] if parents + else ordered_list).append(t) + + # Current level is > last item's level, + # So make last item a parent and append current as child + else: + last['children'].append(t) + parents.append(last) + levels.append(current_level) + last = t + + return ordered_list + + +class TocTreeprocessor(Treeprocessor): + def __init__(self, md, config): + super().__init__(md) + + self.marker = config["marker"] + self.title = config["title"] + self.base_level = int(config["baselevel"]) - 1 + self.slugify = config["slugify"] + self.sep = config["separator"] + self.use_anchors = parseBoolValue(config["anchorlink"]) + self.anchorlink_class = config["anchorlink_class"] + self.use_permalinks = parseBoolValue(config["permalink"], False) + if self.use_permalinks is None: + self.use_permalinks = config["permalink"] + self.permalink_class = config["permalink_class"] + self.permalink_title = config["permalink_title"] + self.header_rgx = re.compile("[Hh][123456]") + if isinstance(config["toc_depth"], str) and '-' in config["toc_depth"]: + self.toc_top, self.toc_bottom = [int(x) for x in config["toc_depth"].split('-')] + else: + self.toc_top = 1 + self.toc_bottom = int(config["toc_depth"]) + + def iterparent(self, node): + ''' Iterator wrapper to get allowed parent and child all at once. ''' + + # We do not allow the marker inside a header as that + # would causes an enless loop of placing a new TOC + # inside previously generated TOC. + for child in node: + if not self.header_rgx.match(child.tag) and child.tag not in ['pre', 'code']: + yield node, child + yield from self.iterparent(child) + + def replace_marker(self, root, elem): + ''' Replace marker with elem. ''' + for (p, c) in self.iterparent(root): + text = ''.join(c.itertext()).strip() + if not text: + continue + + # To keep the output from screwing up the + # validation by putting a <div> inside of a <p> + # we actually replace the <p> in its entirety. + + # The <p> element may contain more than a single text content + # (nl2br can introduce a <br>). In this situation, c.text returns + # the very first content, ignore children contents or tail content. + # len(c) == 0 is here to ensure there is only text in the <p>. + if c.text and c.text.strip() == self.marker and len(c) == 0: + for i in range(len(p)): + if p[i] == c: + p[i] = elem + break + + def set_level(self, elem): + ''' Adjust header level according to base level. ''' + level = int(elem.tag[-1]) + self.base_level + if level > 6: + level = 6 + elem.tag = 'h%d' % level + + def add_anchor(self, c, elem_id): # @ReservedAssignment + anchor = etree.Element("a") + anchor.text = c.text + anchor.attrib["href"] = "#" + elem_id + anchor.attrib["class"] = self.anchorlink_class + c.text = "" + for elem in c: + anchor.append(elem) + while len(c): + c.remove(c[0]) + c.append(anchor) + + def add_permalink(self, c, elem_id): + permalink = etree.Element("a") + permalink.text = ("%spara;" % AMP_SUBSTITUTE + if self.use_permalinks is True + else self.use_permalinks) + permalink.attrib["href"] = "#" + elem_id + permalink.attrib["class"] = self.permalink_class + if self.permalink_title: + permalink.attrib["title"] = self.permalink_title + c.append(permalink) + + def build_toc_div(self, toc_list): + """ Return a string div given a toc list. """ + div = etree.Element("div") + div.attrib["class"] = "toc" + + # Add title to the div + if self.title: + header = etree.SubElement(div, "span") + header.attrib["class"] = "toctitle" + header.text = self.title + + def build_etree_ul(toc_list, parent): + ul = etree.SubElement(parent, "ul") + for item in toc_list: + # List item link, to be inserted into the toc div + li = etree.SubElement(ul, "li") + link = etree.SubElement(li, "a") + link.text = item.get('name', '') + link.attrib["href"] = '#' + item.get('id', '') + if item['children']: + build_etree_ul(item['children'], li) + return ul + + build_etree_ul(toc_list, div) + + if 'prettify' in self.md.treeprocessors: + self.md.treeprocessors['prettify'].run(div) + + return div + + def run(self, doc): + # Get a list of id attributes + used_ids = set() + for el in doc.iter(): + if "id" in el.attrib: + used_ids.add(el.attrib["id"]) + + toc_tokens = [] + for el in doc.iter(): + if isinstance(el.tag, str) and self.header_rgx.match(el.tag): + self.set_level(el) + text = get_name(el) + + # Do not override pre-existing ids + if "id" not in el.attrib: + innertext = unescape(stashedHTML2text(text, self.md)) + el.attrib["id"] = unique(self.slugify(innertext, self.sep), used_ids) + + if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom: + toc_tokens.append({ + 'level': int(el.tag[-1]), + 'id': el.attrib["id"], + 'name': unescape(stashedHTML2text( + code_escape(el.attrib.get('data-toc-label', text)), + self.md, strip_entities=False + )) + }) + + # Remove the data-toc-label attribute as it is no longer needed + if 'data-toc-label' in el.attrib: + del el.attrib['data-toc-label'] + + if self.use_anchors: + self.add_anchor(el, el.attrib["id"]) + if self.use_permalinks not in [False, None]: + self.add_permalink(el, el.attrib["id"]) + + toc_tokens = nest_toc_tokens(toc_tokens) + div = self.build_toc_div(toc_tokens) + if self.marker: + self.replace_marker(doc, div) + + # serialize and attach to markdown instance. + toc = self.md.serializer(div) + for pp in self.md.postprocessors: + toc = pp.run(toc) + self.md.toc_tokens = toc_tokens + self.md.toc = toc + + +class TocExtension(Extension): + + TreeProcessorClass = TocTreeprocessor + + def __init__(self, **kwargs): + self.config = { + "marker": ['[TOC]', + 'Text to find and replace with Table of Contents - ' + 'Set to an empty string to disable. Defaults to "[TOC]"'], + "title": ["", + "Title to insert into TOC <div> - " + "Defaults to an empty string"], + "anchorlink": [False, + "True if header should be a self link - " + "Defaults to False"], + "anchorlink_class": ['toclink', + 'CSS class(es) used for the link. ' + 'Defaults to "toclink"'], + "permalink": [0, + "True or link text if a Sphinx-style permalink should " + "be added - Defaults to False"], + "permalink_class": ['headerlink', + 'CSS class(es) used for the link. ' + 'Defaults to "headerlink"'], + "permalink_title": ["Permanent link", + "Title attribute of the permalink - " + "Defaults to 'Permanent link'"], + "baselevel": ['1', 'Base level for headers.'], + "slugify": [slugify, + "Function to generate anchors based on header text - " + "Defaults to the headerid ext's slugify function."], + 'separator': ['-', 'Word separator. Defaults to "-".'], + "toc_depth": [6, + 'Define the range of section levels to include in' + 'the Table of Contents. A single integer (b) defines' + 'the bottom section level (<h1>..<hb>) only.' + 'A string consisting of two digits separated by a hyphen' + 'in between ("2-5"), define the top (t) and the' + 'bottom (b) (<ht>..<hb>). Defaults to `6` (bottom).'], + } + + super().__init__(**kwargs) + + def extendMarkdown(self, md): + md.registerExtension(self) + self.md = md + self.reset() + tocext = self.TreeProcessorClass(md, self.getConfigs()) + # Headerid ext is set to '>prettify'. With this set to '_end', + # it should always come after headerid ext (and honor ids assigned + # by the header id extension) if both are used. Same goes for + # attr_list extension. This must come last because we don't want + # to redefine ids after toc is created. But we do want toc prettified. + md.treeprocessors.register(tocext, 'toc', 5) + + def reset(self): + self.md.toc = '' + self.md.toc_tokens = [] + + +def makeExtension(**kwargs): # pragma: no cover + return TocExtension(**kwargs) diff --git a/Libs/markdown/extensions/wikilinks.py b/Libs/markdown/extensions/wikilinks.py new file mode 100644 index 0000000..cddee7a --- /dev/null +++ b/Libs/markdown/extensions/wikilinks.py @@ -0,0 +1,87 @@ +''' +WikiLinks Extension for Python-Markdown +====================================== + +Converts [[WikiLinks]] to relative links. + +See <https://Python-Markdown.github.io/extensions/wikilinks> +for documentation. + +Original code Copyright [Waylan Limberg](http://achinghead.com/). + +All changes Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +''' + +from . import Extension +from ..inlinepatterns import InlineProcessor +import xml.etree.ElementTree as etree +import re + + +def build_url(label, base, end): + """ Build a url from the label, a base, and an end. """ + clean_label = re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', label) + return '{}{}{}'.format(base, clean_label, end) + + +class WikiLinkExtension(Extension): + + def __init__(self, **kwargs): + self.config = { + 'base_url': ['/', 'String to append to beginning or URL.'], + 'end_url': ['/', 'String to append to end of URL.'], + 'html_class': ['wikilink', 'CSS hook. Leave blank for none.'], + 'build_url': [build_url, 'Callable formats URL from label.'], + } + + super().__init__(**kwargs) + + def extendMarkdown(self, md): + self.md = md + + # append to end of inline patterns + WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]' + wikilinkPattern = WikiLinksInlineProcessor(WIKILINK_RE, self.getConfigs()) + wikilinkPattern.md = md + md.inlinePatterns.register(wikilinkPattern, 'wikilink', 75) + + +class WikiLinksInlineProcessor(InlineProcessor): + def __init__(self, pattern, config): + super().__init__(pattern) + self.config = config + + def handleMatch(self, m, data): + if m.group(1).strip(): + base_url, end_url, html_class = self._getMeta() + label = m.group(1).strip() + url = self.config['build_url'](label, base_url, end_url) + a = etree.Element('a') + a.text = label + a.set('href', url) + if html_class: + a.set('class', html_class) + else: + a = '' + return a, m.start(0), m.end(0) + + def _getMeta(self): + """ Return meta data or config data. """ + base_url = self.config['base_url'] + end_url = self.config['end_url'] + html_class = self.config['html_class'] + if hasattr(self.md, 'Meta'): + if 'wiki_base_url' in self.md.Meta: + base_url = self.md.Meta['wiki_base_url'][0] + if 'wiki_end_url' in self.md.Meta: + end_url = self.md.Meta['wiki_end_url'][0] + if 'wiki_html_class' in self.md.Meta: + html_class = self.md.Meta['wiki_html_class'][0] + return base_url, end_url, html_class + + +def makeExtension(**kwargs): # pragma: no cover + return WikiLinkExtension(**kwargs) diff --git a/Libs/markdown/htmlparser.py b/Libs/markdown/htmlparser.py new file mode 100644 index 0000000..7ca858e --- /dev/null +++ b/Libs/markdown/htmlparser.py @@ -0,0 +1,323 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2020 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import re +import importlib +import sys + + +# Import a copy of the html.parser lib as `htmlparser` so we can monkeypatch it. +# Users can still do `from html import parser` and get the default behavior. +spec = importlib.util.find_spec('html.parser') +htmlparser = importlib.util.module_from_spec(spec) +spec.loader.exec_module(htmlparser) +sys.modules['htmlparser'] = htmlparser + +# Monkeypatch HTMLParser to only accept `?>` to close Processing Instructions. +htmlparser.piclose = re.compile(r'\?>') +# Monkeypatch HTMLParser to only recognize entity references with a closing semicolon. +htmlparser.entityref = re.compile(r'&([a-zA-Z][-.a-zA-Z0-9]*);') +# Monkeypatch HTMLParser to no longer support partial entities. We are always feeding a complete block, +# so the 'incomplete' functionality is unnecessary. As the entityref regex is run right before incomplete, +# and the two regex are the same, then incomplete will simply never match and we avoid the logic within. +htmlparser.incomplete = htmlparser.entityref +# Monkeypatch HTMLParser to not accept a backtick in a tag name, attribute name, or bare value. +htmlparser.locatestarttagend_tolerant = re.compile(r""" + <[a-zA-Z][^`\t\n\r\f />\x00]* # tag name <= added backtick here + (?:[\s/]* # optional whitespace before attribute name + (?:(?<=['"\s/])[^`\s/>][^\s/=>]* # attribute name <= added backtick here + (?:\s*=+\s* # value indicator + (?:'[^']*' # LITA-enclosed value + |"[^"]*" # LIT-enclosed value + |(?!['"])[^`>\s]* # bare value <= added backtick here + ) + (?:\s*,)* # possibly followed by a comma + )?(?:\s|/(?!>))* + )* + )? + \s* # trailing whitespace +""", re.VERBOSE) + +# Match a blank line at the start of a block of text (two newlines). +# The newlines may be preceded by additional whitespace. +blank_line_re = re.compile(r'^([ ]*\n){2}') + + +class HTMLExtractor(htmlparser.HTMLParser): + """ + Extract raw HTML from text. + + The raw HTML is stored in the `htmlStash` of the Markdown instance passed + to `md` and the remaining text is stored in `cleandoc` as a list of strings. + """ + + def __init__(self, md, *args, **kwargs): + if 'convert_charrefs' not in kwargs: + kwargs['convert_charrefs'] = False + + # Block tags that should contain no content (self closing) + self.empty_tags = set(['hr']) + + # This calls self.reset + super().__init__(*args, **kwargs) + self.md = md + + def reset(self): + """Reset this instance. Loses all unprocessed data.""" + self.inraw = False + self.intail = False + self.stack = [] # When inraw==True, stack contains a list of tags + self._cache = [] + self.cleandoc = [] + super().reset() + + def close(self): + """Handle any buffered data.""" + super().close() + if len(self.rawdata): + # Temp fix for https://bugs.python.org/issue41989 + # TODO: remove this when the bug is fixed in all supported Python versions. + if self.convert_charrefs and not self.cdata_elem: # pragma: no cover + self.handle_data(htmlparser.unescape(self.rawdata)) + else: + self.handle_data(self.rawdata) + # Handle any unclosed tags. + if len(self._cache): + self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache))) + self._cache = [] + + @property + def line_offset(self): + """Returns char index in self.rawdata for the start of the current line. """ + if self.lineno > 1 and '\n' in self.rawdata: + m = re.match(r'([^\n]*\n){{{}}}'.format(self.lineno-1), self.rawdata) + if m: + return m.end() + else: # pragma: no cover + # Value of self.lineno must exceed total number of lines. + # Find index of beginning of last line. + return self.rawdata.rfind('\n') + return 0 + + def at_line_start(self): + """ + Returns True if current position is at start of line. + + Allows for up to three blank spaces at start of line. + """ + if self.offset == 0: + return True + if self.offset > 3: + return False + # Confirm up to first 3 chars are whitespace + return self.rawdata[self.line_offset:self.line_offset + self.offset].strip() == '' + + def get_endtag_text(self, tag): + """ + Returns the text of the end tag. + + If it fails to extract the actual text from the raw data, it builds a closing tag with `tag`. + """ + # Attempt to extract actual tag from raw source text + start = self.line_offset + self.offset + m = htmlparser.endendtag.search(self.rawdata, start) + if m: + return self.rawdata[start:m.end()] + else: # pragma: no cover + # Failed to extract from raw data. Assume well formed and lowercase. + return '</{}>'.format(tag) + + def handle_starttag(self, tag, attrs): + # Handle tags that should always be empty and do not specify a closing tag + if tag in self.empty_tags: + self.handle_startendtag(tag, attrs) + return + + if self.md.is_block_level(tag) and (self.intail or (self.at_line_start() and not self.inraw)): + # Started a new raw block. Prepare stack. + self.inraw = True + self.cleandoc.append('\n') + + text = self.get_starttag_text() + if self.inraw: + self.stack.append(tag) + self._cache.append(text) + else: + self.cleandoc.append(text) + if tag in self.CDATA_CONTENT_ELEMENTS: + # This is presumably a standalone tag in a code span (see #1036). + self.clear_cdata_mode() + + def handle_endtag(self, tag): + text = self.get_endtag_text(tag) + + if self.inraw: + self._cache.append(text) + if tag in self.stack: + # Remove tag from stack + while self.stack: + if self.stack.pop() == tag: + break + if len(self.stack) == 0: + # End of raw block. + if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(text):]): + # Preserve blank line and end of raw block. + self._cache.append('\n') + else: + # More content exists after endtag. + self.intail = True + # Reset stack. + self.inraw = False + self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache))) + # Insert blank line between this and next line. + self.cleandoc.append('\n\n') + self._cache = [] + else: + self.cleandoc.append(text) + + def handle_data(self, data): + if self.intail and '\n' in data: + self.intail = False + if self.inraw: + self._cache.append(data) + else: + self.cleandoc.append(data) + + def handle_empty_tag(self, data, is_block): + """ Handle empty tags (`<data>`). """ + if self.inraw or self.intail: + # Append this to the existing raw block + self._cache.append(data) + elif self.at_line_start() and is_block: + # Handle this as a standalone raw block + if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(data):]): + # Preserve blank line after tag in raw block. + data += '\n' + else: + # More content exists after tag. + self.intail = True + item = self.cleandoc[-1] if self.cleandoc else '' + # If we only have one newline before block element, add another + if not item.endswith('\n\n') and item.endswith('\n'): + self.cleandoc.append('\n') + self.cleandoc.append(self.md.htmlStash.store(data)) + # Insert blank line between this and next line. + self.cleandoc.append('\n\n') + else: + self.cleandoc.append(data) + + def handle_startendtag(self, tag, attrs): + self.handle_empty_tag(self.get_starttag_text(), is_block=self.md.is_block_level(tag)) + + def handle_charref(self, name): + self.handle_empty_tag('&#{};'.format(name), is_block=False) + + def handle_entityref(self, name): + self.handle_empty_tag('&{};'.format(name), is_block=False) + + def handle_comment(self, data): + self.handle_empty_tag('<!--{}-->'.format(data), is_block=True) + + def handle_decl(self, data): + self.handle_empty_tag('<!{}>'.format(data), is_block=True) + + def handle_pi(self, data): + self.handle_empty_tag('<?{}?>'.format(data), is_block=True) + + def unknown_decl(self, data): + end = ']]>' if data.startswith('CDATA[') else ']>' + self.handle_empty_tag('<![{}{}'.format(data, end), is_block=True) + + def parse_pi(self, i): + if self.at_line_start() or self.intail: + return super().parse_pi(i) + # This is not the beginning of a raw block so treat as plain data + # and avoid consuming any tags which may follow (see #1066). + self.handle_data('<?') + return i + 2 + + def parse_html_declaration(self, i): + if self.at_line_start() or self.intail: + return super().parse_html_declaration(i) + # This is not the beginning of a raw block so treat as plain data + # and avoid consuming any tags which may follow (see #1066). + self.handle_data('<!') + return i + 2 + + # The rest has been copied from base class in standard lib to address #1036. + # As __startag_text is private, all references to it must be in this subclass. + # The last few lines of parse_starttag are reversed so that handle_starttag + # can override cdata_mode in certain situations (in a code span). + __starttag_text = None + + def get_starttag_text(self): + """Return full source of start tag: '<...>'.""" + return self.__starttag_text + + def parse_starttag(self, i): # pragma: no cover + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] + + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = htmlparser.tagfind_tolerant.match(rawdata, i+1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = match.group(1).lower() + while k < endpos: + m = htmlparser.attrfind_tolerant.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: # noqa: E127 + attrvalue = attrvalue[1:-1] + if attrvalue: + attrvalue = htmlparser.unescape(attrvalue) + attrs.append((attrname.lower(), attrvalue)) + k = m.end() + + end = rawdata[k:endpos].strip() + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + self.__starttag_text.count("\n") + offset = len(self.__starttag_text) \ + - self.__starttag_text.rfind("\n") # noqa: E127 + else: + offset = offset + len(self.__starttag_text) + self.handle_data(rawdata[i:endpos]) + return endpos + if end.endswith('/>'): + # XHTML-style empty tag: <span attr="value" /> + self.handle_startendtag(tag, attrs) + else: + # *** set cdata_mode first so we can override it in handle_starttag (see #1036) *** + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode(tag) + self.handle_starttag(tag, attrs) + return endpos diff --git a/Libs/markdown/inlinepatterns.py b/Libs/markdown/inlinepatterns.py new file mode 100644 index 0000000..eb74f49 --- /dev/null +++ b/Libs/markdown/inlinepatterns.py @@ -0,0 +1,892 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). + +INLINE PATTERNS +============================================================================= + +Inline patterns such as *emphasis* are handled by means of auxiliary +objects, one per pattern. Pattern objects must be instances of classes +that extend markdown.Pattern. Each pattern object uses a single regular +expression and needs support the following methods: + + pattern.getCompiledRegExp() # returns a regular expression + + pattern.handleMatch(m) # takes a match object and returns + # an ElementTree element or just plain text + +All of python markdown's built-in patterns subclass from Pattern, +but you can add additional patterns that don't. + +Also note that all the regular expressions used by inline must +capture the whole block. For this reason, they all start with +'^(.*)' and end with '(.*)!'. In case with built-in expression +Pattern takes care of adding the "^(.*)" and "(.*)!". + +Finally, the order in which regular expressions are applied is very +important - e.g. if we first replace http://.../ links with <a> tags +and _then_ try to replace inline html, we would end up with a mess. +So, we apply the expressions in the following order: + +* escape and backticks have to go before everything else, so + that we can preempt any markdown patterns by escaping them. + +* then we handle auto-links (must be done before inline html) + +* then we handle inline HTML. At this point we will simply + replace all inline HTML strings with a placeholder and add + the actual HTML to a hash. + +* then inline images (must be done before links) + +* then bracketed links, first regular then reference-style + +* finally we apply strong and emphasis +""" + +from . import util +from collections import namedtuple +import re +import xml.etree.ElementTree as etree +try: # pragma: no cover + from html import entities +except ImportError: # pragma: no cover + import htmlentitydefs as entities + + +def build_inlinepatterns(md, **kwargs): + """ Build the default set of inline patterns for Markdown. """ + inlinePatterns = util.Registry() + inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190) + inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md), 'escape', 180) + inlinePatterns.register(ReferenceInlineProcessor(REFERENCE_RE, md), 'reference', 170) + inlinePatterns.register(LinkInlineProcessor(LINK_RE, md), 'link', 160) + inlinePatterns.register(ImageInlineProcessor(IMAGE_LINK_RE, md), 'image_link', 150) + inlinePatterns.register( + ImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'image_reference', 140 + ) + inlinePatterns.register( + ShortReferenceInlineProcessor(REFERENCE_RE, md), 'short_reference', 130 + ) + inlinePatterns.register( + ShortImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'short_image_ref', 125 + ) + inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md), 'autolink', 120) + inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110) + inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100) + inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md), 'html', 90) + inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md), 'entity', 80) + inlinePatterns.register(SimpleTextInlineProcessor(NOT_STRONG_RE), 'not_strong', 70) + inlinePatterns.register(AsteriskProcessor(r'\*'), 'em_strong', 60) + inlinePatterns.register(UnderscoreProcessor(r'_'), 'em_strong2', 50) + return inlinePatterns + + +""" +The actual regular expressions for patterns +----------------------------------------------------------------------------- +""" + +NOIMG = r'(?<!\!)' + +# `e=f()` or ``e=f("`")`` +BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)(`+)(.+?)(?<!`)\2(?!`))' + +# \< +ESCAPE_RE = r'\\(.)' + +# *emphasis* +EMPHASIS_RE = r'(\*)([^\*]+)\1' + +# **strong** +STRONG_RE = r'(\*{2})(.+?)\1' + +# __smart__strong__ +SMART_STRONG_RE = r'(?<!\w)(_{2})(?!_)(.+?)(?<!_)\1(?!\w)' + +# _smart_emphasis_ +SMART_EMPHASIS_RE = r'(?<!\w)(_)(?!_)(.+?)(?<!_)\1(?!\w)' + +# __strong _em__ +SMART_STRONG_EM_RE = r'(?<!\w)(\_)\1(?!\1)(.+?)(?<!\w)\1(?!\1)(.+?)\1{3}(?!\w)' + +# ***strongem*** or ***em*strong** +EM_STRONG_RE = r'(\*)\1{2}(.+?)\1(.*?)\1{2}' + +# ___strongem___ or ___em_strong__ +EM_STRONG2_RE = r'(_)\1{2}(.+?)\1(.*?)\1{2}' + +# ***strong**em* +STRONG_EM_RE = r'(\*)\1{2}(.+?)\1{2}(.*?)\1' + +# ___strong__em_ +STRONG_EM2_RE = r'(_)\1{2}(.+?)\1{2}(.*?)\1' + +# **strong*em*** +STRONG_EM3_RE = r'(\*)\1(?!\1)([^*]+?)\1(?!\1)(.+?)\1{3}' + +# [text](url) or [text](<url>) or [text](url "title") +LINK_RE = NOIMG + r'\[' + +# ![alttxt](http://x.com/) or ![alttxt](<http://x.com/>) +IMAGE_LINK_RE = r'\!\[' + +# [Google][3] +REFERENCE_RE = LINK_RE + +# ![alt text][2] +IMAGE_REFERENCE_RE = IMAGE_LINK_RE + +# stand-alone * or _ +NOT_STRONG_RE = r'((^|\s)(\*|_)(\s|$))' + +# <http://www.123.com> +AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>' + +# <me@example.com> +AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>' + +# <...> +HTML_RE = r'(<(\/?[a-zA-Z][^<>@ ]*( [^<>]*)?|!--(?:(?!<!--|-->).)*--)>)' + +# "&" (decimal) or "&" (hex) or "&" (named) +ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)' + +# two spaces at end of line +LINE_BREAK_RE = r' \n' + + +def dequote(string): + """Remove quotes from around a string.""" + if ((string.startswith('"') and string.endswith('"')) or + (string.startswith("'") and string.endswith("'"))): + return string[1:-1] + else: + return string + + +class EmStrongItem(namedtuple('EmStrongItem', ['pattern', 'builder', 'tags'])): + """Emphasis/strong pattern item.""" + + +""" +The pattern classes +----------------------------------------------------------------------------- +""" + + +class Pattern: # pragma: no cover + """Base class that inline patterns subclass. """ + + ANCESTOR_EXCLUDES = tuple() + + def __init__(self, pattern, md=None): + """ + Create an instant of an inline pattern. + + Keyword arguments: + + * pattern: A regular expression that matches a pattern + + """ + self.pattern = pattern + self.compiled_re = re.compile(r"^(.*?)%s(.*)$" % pattern, + re.DOTALL | re.UNICODE) + + self.md = md + + @property + @util.deprecated("Use 'md' instead.") + def markdown(self): + # TODO: remove this later + return self.md + + def getCompiledRegExp(self): + """ Return a compiled regular expression. """ + return self.compiled_re + + def handleMatch(self, m): + """Return a ElementTree element from the given match. + + Subclasses should override this method. + + Keyword arguments: + + * m: A re match object containing a match of the pattern. + + """ + pass # pragma: no cover + + def type(self): + """ Return class name, to define pattern type """ + return self.__class__.__name__ + + def unescape(self, text): + """ Return unescaped text given text with an inline placeholder. """ + try: + stash = self.md.treeprocessors['inline'].stashed_nodes + except KeyError: # pragma: no cover + return text + + def get_stash(m): + id = m.group(1) + if id in stash: + value = stash.get(id) + if isinstance(value, str): + return value + else: + # An etree Element - return text content only + return ''.join(value.itertext()) + return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text) + + +class InlineProcessor(Pattern): + """ + Base class that inline patterns subclass. + + This is the newer style inline processor that uses a more + efficient and flexible search approach. + """ + + def __init__(self, pattern, md=None): + """ + Create an instant of an inline pattern. + + Keyword arguments: + + * pattern: A regular expression that matches a pattern + + """ + self.pattern = pattern + self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE) + + # Api for Markdown to pass safe_mode into instance + self.safe_mode = False + self.md = md + + def handleMatch(self, m, data): + """Return a ElementTree element from the given match and the + start and end index of the matched text. + + If `start` and/or `end` are returned as `None`, it will be + assumed that the processor did not find a valid region of text. + + Subclasses should override this method. + + Keyword arguments: + + * m: A re match object containing a match of the pattern. + * data: The buffer current under analysis + + Returns: + + * el: The ElementTree element, text or None. + * start: The start of the region that has been matched or None. + * end: The end of the region that has been matched or None. + + """ + pass # pragma: no cover + + +class SimpleTextPattern(Pattern): # pragma: no cover + """ Return a simple text of group(2) of a Pattern. """ + def handleMatch(self, m): + return m.group(2) + + +class SimpleTextInlineProcessor(InlineProcessor): + """ Return a simple text of group(1) of a Pattern. """ + def handleMatch(self, m, data): + return m.group(1), m.start(0), m.end(0) + + +class EscapeInlineProcessor(InlineProcessor): + """ Return an escaped character. """ + + def handleMatch(self, m, data): + char = m.group(1) + if char in self.md.ESCAPED_CHARS: + return '{}{}{}'.format(util.STX, ord(char), util.ETX), m.start(0), m.end(0) + else: + return None, m.start(0), m.end(0) + + +class SimpleTagPattern(Pattern): # pragma: no cover + """ + Return element of type `tag` with a text attribute of group(3) + of a Pattern. + + """ + def __init__(self, pattern, tag): + Pattern.__init__(self, pattern) + self.tag = tag + + def handleMatch(self, m): + el = etree.Element(self.tag) + el.text = m.group(3) + return el + + +class SimpleTagInlineProcessor(InlineProcessor): + """ + Return element of type `tag` with a text attribute of group(2) + of a Pattern. + + """ + def __init__(self, pattern, tag): + InlineProcessor.__init__(self, pattern) + self.tag = tag + + def handleMatch(self, m, data): # pragma: no cover + el = etree.Element(self.tag) + el.text = m.group(2) + return el, m.start(0), m.end(0) + + +class SubstituteTagPattern(SimpleTagPattern): # pragma: no cover + """ Return an element of type `tag` with no children. """ + def handleMatch(self, m): + return etree.Element(self.tag) + + +class SubstituteTagInlineProcessor(SimpleTagInlineProcessor): + """ Return an element of type `tag` with no children. """ + def handleMatch(self, m, data): + return etree.Element(self.tag), m.start(0), m.end(0) + + +class BacktickInlineProcessor(InlineProcessor): + """ Return a `<code>` element containing the matching text. """ + def __init__(self, pattern): + InlineProcessor.__init__(self, pattern) + self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX) + self.tag = 'code' + + def handleMatch(self, m, data): + if m.group(3): + el = etree.Element(self.tag) + el.text = util.AtomicString(util.code_escape(m.group(3).strip())) + return el, m.start(0), m.end(0) + else: + return m.group(1).replace('\\\\', self.ESCAPED_BSLASH), m.start(0), m.end(0) + + +class DoubleTagPattern(SimpleTagPattern): # pragma: no cover + """Return a ElementTree element nested in tag2 nested in tag1. + + Useful for strong emphasis etc. + + """ + def handleMatch(self, m): + tag1, tag2 = self.tag.split(",") + el1 = etree.Element(tag1) + el2 = etree.SubElement(el1, tag2) + el2.text = m.group(3) + if len(m.groups()) == 5: + el2.tail = m.group(4) + return el1 + + +class DoubleTagInlineProcessor(SimpleTagInlineProcessor): + """Return a ElementTree element nested in tag2 nested in tag1. + + Useful for strong emphasis etc. + + """ + def handleMatch(self, m, data): # pragma: no cover + tag1, tag2 = self.tag.split(",") + el1 = etree.Element(tag1) + el2 = etree.SubElement(el1, tag2) + el2.text = m.group(2) + if len(m.groups()) == 3: + el2.tail = m.group(3) + return el1, m.start(0), m.end(0) + + +class HtmlInlineProcessor(InlineProcessor): + """ Store raw inline html and return a placeholder. """ + def handleMatch(self, m, data): + rawhtml = self.unescape(m.group(1)) + place_holder = self.md.htmlStash.store(rawhtml) + return place_holder, m.start(0), m.end(0) + + def unescape(self, text): + """ Return unescaped text given text with an inline placeholder. """ + try: + stash = self.md.treeprocessors['inline'].stashed_nodes + except KeyError: # pragma: no cover + return text + + def get_stash(m): + id = m.group(1) + value = stash.get(id) + if value is not None: + try: + return self.md.serializer(value) + except Exception: + return r'\%s' % value + + return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text) + + +class AsteriskProcessor(InlineProcessor): + """Emphasis processor for handling strong and em matches inside asterisks.""" + + PATTERNS = [ + EmStrongItem(re.compile(EM_STRONG_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), + EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), + EmStrongItem(re.compile(STRONG_EM3_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), + EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), + EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') + ] + + def build_single(self, m, tag, idx): + """Return single tag.""" + el1 = etree.Element(tag) + text = m.group(2) + self.parse_sub_patterns(text, el1, None, idx) + return el1 + + def build_double(self, m, tags, idx): + """Return double tag.""" + + tag1, tag2 = tags.split(",") + el1 = etree.Element(tag1) + el2 = etree.Element(tag2) + text = m.group(2) + self.parse_sub_patterns(text, el2, None, idx) + el1.append(el2) + if len(m.groups()) == 3: + text = m.group(3) + self.parse_sub_patterns(text, el1, el2, idx) + return el1 + + def build_double2(self, m, tags, idx): + """Return double tags (variant 2): `<strong>text <em>text</em></strong>`.""" + + tag1, tag2 = tags.split(",") + el1 = etree.Element(tag1) + el2 = etree.Element(tag2) + text = m.group(2) + self.parse_sub_patterns(text, el1, None, idx) + text = m.group(3) + el1.append(el2) + self.parse_sub_patterns(text, el2, None, idx) + return el1 + + def parse_sub_patterns(self, data, parent, last, idx): + """ + Parses sub patterns. + + `data` (`str`): + text to evaluate. + + `parent` (`etree.Element`): + Parent to attach text and sub elements to. + + `last` (`etree.Element`): + Last appended child to parent. Can also be None if parent has no children. + + `idx` (`int`): + Current pattern index that was used to evaluate the parent. + + """ + + offset = 0 + pos = 0 + + length = len(data) + while pos < length: + # Find the start of potential emphasis or strong tokens + if self.compiled_re.match(data, pos): + matched = False + # See if the we can match an emphasis/strong pattern + for index, item in enumerate(self.PATTERNS): + # Only evaluate patterns that are after what was used on the parent + if index <= idx: + continue + m = item.pattern.match(data, pos) + if m: + # Append child nodes to parent + # Text nodes should be appended to the last + # child if present, and if not, it should + # be added as the parent's text node. + text = data[offset:m.start(0)] + if text: + if last is not None: + last.tail = text + else: + parent.text = text + el = self.build_element(m, item.builder, item.tags, index) + parent.append(el) + last = el + # Move our position past the matched hunk + offset = pos = m.end(0) + matched = True + if not matched: + # We matched nothing, move on to the next character + pos += 1 + else: + # Increment position as no potential emphasis start was found. + pos += 1 + + # Append any leftover text as a text node. + text = data[offset:] + if text: + if last is not None: + last.tail = text + else: + parent.text = text + + def build_element(self, m, builder, tags, index): + """Element builder.""" + + if builder == 'double2': + return self.build_double2(m, tags, index) + elif builder == 'double': + return self.build_double(m, tags, index) + else: + return self.build_single(m, tags, index) + + def handleMatch(self, m, data): + """Parse patterns.""" + + el = None + start = None + end = None + + for index, item in enumerate(self.PATTERNS): + m1 = item.pattern.match(data, m.start(0)) + if m1: + start = m1.start(0) + end = m1.end(0) + el = self.build_element(m1, item.builder, item.tags, index) + break + return el, start, end + + +class UnderscoreProcessor(AsteriskProcessor): + """Emphasis processor for handling strong and em matches inside underscores.""" + + PATTERNS = [ + EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), + EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), + EmStrongItem(re.compile(SMART_STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), + EmStrongItem(re.compile(SMART_STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), + EmStrongItem(re.compile(SMART_EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') + ] + + +class LinkInlineProcessor(InlineProcessor): + """ Return a link element from the given match. """ + RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE) + RE_TITLE_CLEAN = re.compile(r'\s') + + def handleMatch(self, m, data): + text, index, handled = self.getText(data, m.end(0)) + + if not handled: + return None, None, None + + href, title, index, handled = self.getLink(data, index) + if not handled: + return None, None, None + + el = etree.Element("a") + el.text = text + + el.set("href", href) + + if title is not None: + el.set("title", title) + + return el, m.start(0), index + + def getLink(self, data, index): + """Parse data between `()` of `[Text]()` allowing recursive `()`. """ + + href = '' + title = None + handled = False + + m = self.RE_LINK.match(data, pos=index) + if m and m.group(1): + # Matches [Text](<link> "title") + href = m.group(1)[1:-1].strip() + if m.group(2): + title = m.group(2)[1:-1] + index = m.end(0) + handled = True + elif m: + # Track bracket nesting and index in string + bracket_count = 1 + backtrack_count = 1 + start_index = m.end() + index = start_index + last_bracket = -1 + + # Primary (first found) quote tracking. + quote = None + start_quote = -1 + exit_quote = -1 + ignore_matches = False + + # Secondary (second found) quote tracking. + alt_quote = None + start_alt_quote = -1 + exit_alt_quote = -1 + + # Track last character + last = '' + + for pos in range(index, len(data)): + c = data[pos] + if c == '(': + # Count nested ( + # Don't increment the bracket count if we are sure we're in a title. + if not ignore_matches: + bracket_count += 1 + elif backtrack_count > 0: + backtrack_count -= 1 + elif c == ')': + # Match nested ) to ( + # Don't decrement if we are sure we are in a title that is unclosed. + if ((exit_quote != -1 and quote == last) or (exit_alt_quote != -1 and alt_quote == last)): + bracket_count = 0 + elif not ignore_matches: + bracket_count -= 1 + elif backtrack_count > 0: + backtrack_count -= 1 + # We've found our backup end location if the title doesn't resolve. + if backtrack_count == 0: + last_bracket = index + 1 + + elif c in ("'", '"'): + # Quote has started + if not quote: + # We'll assume we are now in a title. + # Brackets are quoted, so no need to match them (except for the final one). + ignore_matches = True + backtrack_count = bracket_count + bracket_count = 1 + start_quote = index + 1 + quote = c + # Secondary quote (in case the first doesn't resolve): [text](link'"title") + elif c != quote and not alt_quote: + start_alt_quote = index + 1 + alt_quote = c + # Update primary quote match + elif c == quote: + exit_quote = index + 1 + # Update secondary quote match + elif alt_quote and c == alt_quote: + exit_alt_quote = index + 1 + + index += 1 + + # Link is closed, so let's break out of the loop + if bracket_count == 0: + # Get the title if we closed a title string right before link closed + if exit_quote >= 0 and quote == last: + href = data[start_index:start_quote - 1] + title = ''.join(data[start_quote:exit_quote - 1]) + elif exit_alt_quote >= 0 and alt_quote == last: + href = data[start_index:start_alt_quote - 1] + title = ''.join(data[start_alt_quote:exit_alt_quote - 1]) + else: + href = data[start_index:index - 1] + break + + if c != ' ': + last = c + + # We have a scenario: [test](link"notitle) + # When we enter a string, we stop tracking bracket resolution in the main counter, + # but we do keep a backup counter up until we discover where we might resolve all brackets + # if the title string fails to resolve. + if bracket_count != 0 and backtrack_count == 0: + href = data[start_index:last_bracket - 1] + index = last_bracket + bracket_count = 0 + + handled = bracket_count == 0 + + if title is not None: + title = self.RE_TITLE_CLEAN.sub(' ', dequote(self.unescape(title.strip()))) + + href = self.unescape(href).strip() + + return href, title, index, handled + + def getText(self, data, index): + """Parse the content between `[]` of the start of an image or link + resolving nested square brackets. + + """ + bracket_count = 1 + text = [] + for pos in range(index, len(data)): + c = data[pos] + if c == ']': + bracket_count -= 1 + elif c == '[': + bracket_count += 1 + index += 1 + if bracket_count == 0: + break + text.append(c) + return ''.join(text), index, bracket_count == 0 + + +class ImageInlineProcessor(LinkInlineProcessor): + """ Return a img element from the given match. """ + + def handleMatch(self, m, data): + text, index, handled = self.getText(data, m.end(0)) + if not handled: + return None, None, None + + src, title, index, handled = self.getLink(data, index) + if not handled: + return None, None, None + + el = etree.Element("img") + + el.set("src", src) + + if title is not None: + el.set("title", title) + + el.set('alt', self.unescape(text)) + return el, m.start(0), index + + +class ReferenceInlineProcessor(LinkInlineProcessor): + """ Match to a stored reference and return link element. """ + NEWLINE_CLEANUP_RE = re.compile(r'\s+', re.MULTILINE) + + RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE) + + def handleMatch(self, m, data): + text, index, handled = self.getText(data, m.end(0)) + if not handled: + return None, None, None + + id, end, handled = self.evalId(data, index, text) + if not handled: + return None, None, None + + # Clean up linebreaks in id + id = self.NEWLINE_CLEANUP_RE.sub(' ', id) + if id not in self.md.references: # ignore undefined refs + return None, m.start(0), end + + href, title = self.md.references[id] + + return self.makeTag(href, title, text), m.start(0), end + + def evalId(self, data, index, text): + """ + Evaluate the id portion of [ref][id]. + + If [ref][] use [ref]. + """ + m = self.RE_LINK.match(data, pos=index) + if not m: + return None, index, False + else: + id = m.group(1).lower() + end = m.end(0) + if not id: + id = text.lower() + return id, end, True + + def makeTag(self, href, title, text): + el = etree.Element('a') + + el.set('href', href) + if title: + el.set('title', title) + + el.text = text + return el + + +class ShortReferenceInlineProcessor(ReferenceInlineProcessor): + """Short form of reference: [google]. """ + def evalId(self, data, index, text): + """Evaluate the id from of [ref] """ + + return text.lower(), index, True + + +class ImageReferenceInlineProcessor(ReferenceInlineProcessor): + """ Match to a stored reference and return img element. """ + def makeTag(self, href, title, text): + el = etree.Element("img") + el.set("src", href) + if title: + el.set("title", title) + el.set("alt", self.unescape(text)) + return el + + +class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor): + """ Short form of inage reference: ![ref]. """ + def evalId(self, data, index, text): + """Evaluate the id from of [ref] """ + + return text.lower(), index, True + + +class AutolinkInlineProcessor(InlineProcessor): + """ Return a link Element given an autolink (`<http://example/com>`). """ + def handleMatch(self, m, data): + el = etree.Element("a") + el.set('href', self.unescape(m.group(1))) + el.text = util.AtomicString(m.group(1)) + return el, m.start(0), m.end(0) + + +class AutomailInlineProcessor(InlineProcessor): + """ + Return a mailto link Element given an automail link (`<foo@example.com>`). + """ + def handleMatch(self, m, data): + el = etree.Element('a') + email = self.unescape(m.group(1)) + if email.startswith("mailto:"): + email = email[len("mailto:"):] + + def codepoint2name(code): + """Return entity definition by code, or the code if not defined.""" + entity = entities.codepoint2name.get(code) + if entity: + return "{}{};".format(util.AMP_SUBSTITUTE, entity) + else: + return "%s#%d;" % (util.AMP_SUBSTITUTE, code) + + letters = [codepoint2name(ord(letter)) for letter in email] + el.text = util.AtomicString(''.join(letters)) + + mailto = "mailto:" + email + mailto = "".join([util.AMP_SUBSTITUTE + '#%d;' % + ord(letter) for letter in mailto]) + el.set('href', mailto) + return el, m.start(0), m.end(0) diff --git a/Libs/markdown/pep562.py b/Libs/markdown/pep562.py new file mode 100644 index 0000000..b130d3b --- /dev/null +++ b/Libs/markdown/pep562.py @@ -0,0 +1,245 @@ +""" +Backport of PEP 562. + +https://pypi.org/search/?q=pep562 + +Licensed under MIT +Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" +import sys +from collections import namedtuple +import re + +__all__ = ('Pep562',) + +RE_VER = re.compile( + r'''(?x) + (?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))? + (?:(?P<type>a|b|rc)(?P<pre>\d+))? + (?:\.post(?P<post>\d+))? + (?:\.dev(?P<dev>\d+))? + ''' +) + +REL_MAP = { + ".dev": "", + ".dev-alpha": "a", + ".dev-beta": "b", + ".dev-candidate": "rc", + "alpha": "a", + "beta": "b", + "candidate": "rc", + "final": "" +} + +DEV_STATUS = { + ".dev": "2 - Pre-Alpha", + ".dev-alpha": "2 - Pre-Alpha", + ".dev-beta": "2 - Pre-Alpha", + ".dev-candidate": "2 - Pre-Alpha", + "alpha": "3 - Alpha", + "beta": "4 - Beta", + "candidate": "4 - Beta", + "final": "5 - Production/Stable" +} + +PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'} + + +class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])): + """ + Get the version (PEP 440). + + A biased approach to the PEP 440 semantic version. + + Provides a tuple structure which is sorted for comparisons `v1 > v2` etc. + (major, minor, micro, release type, pre-release build, post-release build, development release build) + Release types are named in is such a way they are comparable with ease. + Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get + development status for setup files. + + How it works (currently): + + - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`. + - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`. + The dot is used to ensure all development specifiers are sorted before `alpha`. + You can specify a `dev` number for development builds, but do not have to as implicit development releases + are allowed. + - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not + allow implicit prereleases. + - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases + are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be + noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically + does not allow implicit post releases. + - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`. + + Acceptable version releases: + + ``` + Version(1, 0, 0, "final") 1.0 + Version(1, 2, 0, "final") 1.2 + Version(1, 2, 3, "final") 1.2.3 + Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4 + Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4 + Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4 + Version(1, 2, 0, "final", post=1) 1.2.post1 + Version(1, 2, 3, ".dev") 1.2.3.dev0 + Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1 + ``` + + """ + + def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0): + """Validate version info.""" + + # Ensure all parts are positive integers. + for value in (major, minor, micro, pre, post): + if not (isinstance(value, int) and value >= 0): + raise ValueError("All version parts except 'release' should be integers.") + + if release not in REL_MAP: + raise ValueError("'{}' is not a valid release type.".format(release)) + + # Ensure valid pre-release (we do not allow implicit pre-releases). + if ".dev-candidate" < release < "final": + if pre == 0: + raise ValueError("Implicit pre-releases not allowed.") + elif dev: + raise ValueError("Version is not a development release.") + elif post: + raise ValueError("Post-releases are not allowed with pre-releases.") + + # Ensure valid development or development/pre release + elif release < "alpha": + if release > ".dev" and pre == 0: + raise ValueError("Implicit pre-release not allowed.") + elif post: + raise ValueError("Post-releases are not allowed with pre-releases.") + + # Ensure a valid normal release + else: + if pre: + raise ValueError("Version is not a pre-release.") + elif dev: + raise ValueError("Version is not a development release.") + + return super().__new__(cls, major, minor, micro, release, pre, post, dev) + + def _is_pre(self): + """Is prerelease.""" + + return self.pre > 0 + + def _is_dev(self): + """Is development.""" + + return bool(self.release < "alpha") + + def _is_post(self): + """Is post.""" + + return self.post > 0 + + def _get_dev_status(self): # pragma: no cover + """Get development status string.""" + + return DEV_STATUS[self.release] + + def _get_canonical(self): + """Get the canonical output string.""" + + # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed.. + if self.micro == 0: + ver = "{}.{}".format(self.major, self.minor) + else: + ver = "{}.{}.{}".format(self.major, self.minor, self.micro) + if self._is_pre(): + ver += '{}{}'.format(REL_MAP[self.release], self.pre) + if self._is_post(): + ver += ".post{}".format(self.post) + if self._is_dev(): + ver += ".dev{}".format(self.dev) + + return ver + + +def parse_version(ver, pre=False): + """Parse version into a comparable Version tuple.""" + + m = RE_VER.match(ver) + + # Handle major, minor, micro + major = int(m.group('major')) + minor = int(m.group('minor')) if m.group('minor') else 0 + micro = int(m.group('micro')) if m.group('micro') else 0 + + # Handle pre releases + if m.group('type'): + release = PRE_REL_MAP[m.group('type')] + pre = int(m.group('pre')) + else: + release = "final" + pre = 0 + + # Handle development releases + dev = m.group('dev') if m.group('dev') else 0 + if m.group('dev'): + dev = int(m.group('dev')) + release = '.dev-' + release if pre else '.dev' + else: + dev = 0 + + # Handle post + post = int(m.group('post')) if m.group('post') else 0 + + return Version(major, minor, micro, release, pre, post, dev) + + +class Pep562: + """ + Backport of PEP 562 <https://pypi.org/search/?q=pep562>. + + Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`. + The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed. + """ + + def __init__(self, name): + """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7.""" + + self._module = sys.modules[name] + self._get_attr = getattr(self._module, '__getattr__', None) + self._get_dir = getattr(self._module, '__dir__', None) + sys.modules[name] = self + + def __dir__(self): + """Return the overridden `dir` if one was provided, else apply `dir` to the module.""" + + return self._get_dir() if self._get_dir else dir(self._module) + + def __getattr__(self, name): + """Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present.""" + + try: + return getattr(self._module, name) + except AttributeError: + if self._get_attr: + return self._get_attr(name) + raise + + +__version_info__ = Version(1, 0, 0, "final") +__version__ = __version_info__._get_canonical() diff --git a/Libs/markdown/postprocessors.py b/Libs/markdown/postprocessors.py new file mode 100644 index 0000000..f4fb924 --- /dev/null +++ b/Libs/markdown/postprocessors.py @@ -0,0 +1,134 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). + +POST-PROCESSORS +============================================================================= + +Markdown also allows post-processors, which are similar to preprocessors in +that they need to implement a "run" method. However, they are run after core +processing. + +""" + +from collections import OrderedDict +from . import util +import re + + +def build_postprocessors(md, **kwargs): + """ Build the default postprocessors for Markdown. """ + postprocessors = util.Registry() + postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30) + postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20) + postprocessors.register(UnescapePostprocessor(), 'unescape', 10) + return postprocessors + + +class Postprocessor(util.Processor): + """ + Postprocessors are run after the ElementTree it converted back into text. + + Each Postprocessor implements a "run" method that takes a pointer to a + text string, modifies it as necessary and returns a text string. + + Postprocessors must extend markdown.Postprocessor. + + """ + + def run(self, text): + """ + Subclasses of Postprocessor should implement a `run` method, which + takes the html document as a single text string and returns a + (possibly modified) string. + + """ + pass # pragma: no cover + + +class RawHtmlPostprocessor(Postprocessor): + """ Restore raw html to the document. """ + + BLOCK_LEVEL_REGEX = re.compile(r'^\<\/?([^ >]+)') + + def run(self, text): + """ Iterate over html stash and restore html. """ + replacements = OrderedDict() + for i in range(self.md.htmlStash.html_counter): + html = self.stash_to_string(self.md.htmlStash.rawHtmlBlocks[i]) + if self.isblocklevel(html): + replacements["<p>{}</p>".format( + self.md.htmlStash.get_placeholder(i))] = html + replacements[self.md.htmlStash.get_placeholder(i)] = html + + def substitute_match(m): + key = m.group(0) + + if key not in replacements: + if key[3:-4] in replacements: + return f'<p>{ replacements[key[3:-4]] }</p>' + else: + return key + + return replacements[key] + + if replacements: + base_placeholder = util.HTML_PLACEHOLDER % r'([0-9]+)' + pattern = re.compile(f'<p>{ base_placeholder }</p>|{ base_placeholder }') + processed_text = pattern.sub(substitute_match, text) + else: + return text + + if processed_text == text: + return processed_text + else: + return self.run(processed_text) + + def isblocklevel(self, html): + m = self.BLOCK_LEVEL_REGEX.match(html) + if m: + if m.group(1)[0] in ('!', '?', '@', '%'): + # Comment, php etc... + return True + return self.md.is_block_level(m.group(1)) + return False + + def stash_to_string(self, text): + """ Convert a stashed object to a string. """ + return str(text) + + +class AndSubstitutePostprocessor(Postprocessor): + """ Restore valid entities """ + + def run(self, text): + text = text.replace(util.AMP_SUBSTITUTE, "&") + return text + + +class UnescapePostprocessor(Postprocessor): + """ Restore escaped chars """ + + RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX)) + + def unescape(self, m): + return chr(int(m.group(1))) + + def run(self, text): + return self.RE.sub(self.unescape, text) diff --git a/Libs/markdown/preprocessors.py b/Libs/markdown/preprocessors.py new file mode 100644 index 0000000..e1023c5 --- /dev/null +++ b/Libs/markdown/preprocessors.py @@ -0,0 +1,82 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). + +PRE-PROCESSORS +============================================================================= + +Preprocessors work on source text before we start doing anything too +complicated. +""" + +from . import util +from .htmlparser import HTMLExtractor +import re + + +def build_preprocessors(md, **kwargs): + """ Build the default set of preprocessors used by Markdown. """ + preprocessors = util.Registry() + preprocessors.register(NormalizeWhitespace(md), 'normalize_whitespace', 30) + preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20) + return preprocessors + + +class Preprocessor(util.Processor): + """ + Preprocessors are run after the text is broken into lines. + + Each preprocessor implements a "run" method that takes a pointer to a + list of lines of the document, modifies it as necessary and returns + either the same pointer or a pointer to a new list. + + Preprocessors must extend markdown.Preprocessor. + + """ + def run(self, lines): + """ + Each subclass of Preprocessor should override the `run` method, which + takes the document as a list of strings split by newlines and returns + the (possibly modified) list of lines. + + """ + pass # pragma: no cover + + +class NormalizeWhitespace(Preprocessor): + """ Normalize whitespace for consistent parsing. """ + + def run(self, lines): + source = '\n'.join(lines) + source = source.replace(util.STX, "").replace(util.ETX, "") + source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n" + source = source.expandtabs(self.md.tab_length) + source = re.sub(r'(?<=\n) +\n', '\n', source) + return source.split('\n') + + +class HtmlBlockPreprocessor(Preprocessor): + """Remove html blocks from the text and store them for later retrieval.""" + + def run(self, lines): + source = '\n'.join(lines) + parser = HTMLExtractor(self.md) + parser.feed(source) + parser.close() + return ''.join(parser.cleandoc).split('\n') diff --git a/Libs/markdown/serializers.py b/Libs/markdown/serializers.py new file mode 100644 index 0000000..59bab18 --- /dev/null +++ b/Libs/markdown/serializers.py @@ -0,0 +1,189 @@ +# markdown/searializers.py +# +# Add x/html serialization to Elementree +# Taken from ElementTree 1.3 preview with slight modifications +# +# Copyright (c) 1999-2007 by Fredrik Lundh. All rights reserved. +# +# fredrik@pythonware.com +# https://www.pythonware.com/ +# +# -------------------------------------------------------------------- +# The ElementTree toolkit is +# +# Copyright (c) 1999-2007 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + + +from xml.etree.ElementTree import ProcessingInstruction +from xml.etree.ElementTree import Comment, ElementTree, QName +import re + +__all__ = ['to_html_string', 'to_xhtml_string'] + +HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr", + "img", "input", "isindex", "link", "meta", "param") +RE_AMP = re.compile(r'&(?!(?:\#[0-9]+|\#x[0-9a-f]+|[0-9a-z]+);)', re.I) + +try: + HTML_EMPTY = set(HTML_EMPTY) +except NameError: # pragma: no cover + pass + + +def _raise_serialization_error(text): # pragma: no cover + raise TypeError( + "cannot serialize {!r} (type {})".format(text, type(text).__name__) + ) + + +def _escape_cdata(text): + # escape character data + try: + # it's worth avoiding do-nothing calls for strings that are + # shorter than 500 character, or so. assume that's, by far, + # the most common case in most applications. + if "&" in text: + # Only replace & when not part of an entity + text = RE_AMP.sub('&', text) + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + return text + except (TypeError, AttributeError): # pragma: no cover + _raise_serialization_error(text) + + +def _escape_attrib(text): + # escape attribute value + try: + if "&" in text: + # Only replace & when not part of an entity + text = RE_AMP.sub('&', text) + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + if "\"" in text: + text = text.replace("\"", """) + if "\n" in text: + text = text.replace("\n", " ") + return text + except (TypeError, AttributeError): # pragma: no cover + _raise_serialization_error(text) + + +def _escape_attrib_html(text): + # escape attribute value + try: + if "&" in text: + # Only replace & when not part of an entity + text = RE_AMP.sub('&', text) + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + if "\"" in text: + text = text.replace("\"", """) + return text + except (TypeError, AttributeError): # pragma: no cover + _raise_serialization_error(text) + + +def _serialize_html(write, elem, format): + tag = elem.tag + text = elem.text + if tag is Comment: + write("<!--%s-->" % _escape_cdata(text)) + elif tag is ProcessingInstruction: + write("<?%s?>" % _escape_cdata(text)) + elif tag is None: + if text: + write(_escape_cdata(text)) + for e in elem: + _serialize_html(write, e, format) + else: + namespace_uri = None + if isinstance(tag, QName): + # QNAME objects store their data as a string: `{uri}tag` + if tag.text[:1] == "{": + namespace_uri, tag = tag.text[1:].split("}", 1) + else: + raise ValueError('QName objects must define a tag.') + write("<" + tag) + items = elem.items() + if items: + items = sorted(items) # lexical order + for k, v in items: + if isinstance(k, QName): + # Assume a text only QName + k = k.text + if isinstance(v, QName): + # Assume a text only QName + v = v.text + else: + v = _escape_attrib_html(v) + if k == v and format == 'html': + # handle boolean attributes + write(" %s" % v) + else: + write(' {}="{}"'.format(k, v)) + if namespace_uri: + write(' xmlns="%s"' % (_escape_attrib(namespace_uri))) + if format == "xhtml" and tag.lower() in HTML_EMPTY: + write(" />") + else: + write(">") + if text: + if tag.lower() in ["script", "style"]: + write(text) + else: + write(_escape_cdata(text)) + for e in elem: + _serialize_html(write, e, format) + if tag.lower() not in HTML_EMPTY: + write("</" + tag + ">") + if elem.tail: + write(_escape_cdata(elem.tail)) + + +def _write_html(root, format="html"): + assert root is not None + data = [] + write = data.append + _serialize_html(write, root, format) + return "".join(data) + + +# -------------------------------------------------------------------- +# public functions + +def to_html_string(element): + return _write_html(ElementTree(element).getroot(), format="html") + + +def to_xhtml_string(element): + return _write_html(ElementTree(element).getroot(), format="xhtml") diff --git a/Libs/markdown/test_tools.py b/Libs/markdown/test_tools.py new file mode 100644 index 0000000..2ce0e74 --- /dev/null +++ b/Libs/markdown/test_tools.py @@ -0,0 +1,220 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import os +import sys +import unittest +import textwrap +from . import markdown, Markdown, util + +try: + import tidylib +except ImportError: + tidylib = None + +__all__ = ['TestCase', 'LegacyTestCase', 'Kwargs'] + + +class TestCase(unittest.TestCase): + """ + A unittest.TestCase subclass with helpers for testing Markdown output. + + Define `default_kwargs` as a dict of keywords to pass to Markdown for each + test. The defaults can be overridden on individual tests. + + The `assertMarkdownRenders` method accepts the source text, the expected + output, and any keywords to pass to Markdown. The `default_kwargs` are used + except where overridden by `kwargs`. The output and expected output are passed + to `TestCase.assertMultiLineEqual`. An AssertionError is raised with a diff + if the actual output does not equal the expected output. + + The `dedent` method is available to dedent triple-quoted strings if + necessary. + + In all other respects, behaves as unittest.TestCase. + """ + + default_kwargs = {} + + def assertMarkdownRenders(self, source, expected, expected_attrs=None, **kwargs): + """ + Test that source Markdown text renders to expected output with given keywords. + + `expected_attrs` accepts a dict. Each key should be the name of an attribute + on the `Markdown` instance and the value should be the expected value after + the source text is parsed by Markdown. After the expected output is tested, + the expected value for each attribute is compared against the actual + attribute of the `Markdown` instance using `TestCase.assertEqual`. + """ + + expected_attrs = expected_attrs or {} + kws = self.default_kwargs.copy() + kws.update(kwargs) + md = Markdown(**kws) + output = md.convert(source) + self.assertMultiLineEqual(output, expected) + for key, value in expected_attrs.items(): + self.assertEqual(getattr(md, key), value) + + def dedent(self, text): + """ + Dedent text. + """ + + # TODO: If/when actual output ends with a newline, then use: + # return textwrap.dedent(text.strip('/n')) + return textwrap.dedent(text).strip() + + +class recursionlimit: + """ + A context manager which temporarily modifies the Python recursion limit. + + The testing framework, coverage, etc. may add an arbitrary number of levels to the depth. To maintain consistency + in the tests, the current stack depth is determined when called, then added to the provided limit. + + Example usage: + + with recursionlimit(20): + # test code here + + See https://stackoverflow.com/a/50120316/866026 + """ + + def __init__(self, limit): + self.limit = util._get_stack_depth() + limit + self.old_limit = sys.getrecursionlimit() + + def __enter__(self): + sys.setrecursionlimit(self.limit) + + def __exit__(self, type, value, tb): + sys.setrecursionlimit(self.old_limit) + + +######################### +# Legacy Test Framework # +######################### + + +class Kwargs(dict): + """ A dict like class for holding keyword arguments. """ + pass + + +def _normalize_whitespace(text): + """ Normalize whitespace for a string of html using tidylib. """ + output, errors = tidylib.tidy_fragment(text, options={ + 'drop_empty_paras': 0, + 'fix_backslash': 0, + 'fix_bad_comments': 0, + 'fix_uri': 0, + 'join_styles': 0, + 'lower_literals': 0, + 'merge_divs': 0, + 'output_xhtml': 1, + 'quote_ampersand': 0, + 'newline': 'LF' + }) + return output + + +class LegacyTestMeta(type): + def __new__(cls, name, bases, dct): + + def generate_test(infile, outfile, normalize, kwargs): + def test(self): + with open(infile, encoding="utf-8") as f: + input = f.read() + with open(outfile, encoding="utf-8") as f: + # Normalize line endings + # (on Windows, git may have altered line endings). + expected = f.read().replace("\r\n", "\n") + output = markdown(input, **kwargs) + if tidylib and normalize: + try: + expected = _normalize_whitespace(expected) + output = _normalize_whitespace(output) + except OSError: + self.skipTest("Tidylib's c library not available.") + elif normalize: + self.skipTest('Tidylib not available.') + self.assertMultiLineEqual(output, expected) + return test + + location = dct.get('location', '') + exclude = dct.get('exclude', []) + normalize = dct.get('normalize', False) + input_ext = dct.get('input_ext', '.txt') + output_ext = dct.get('output_ext', '.html') + kwargs = dct.get('default_kwargs', Kwargs()) + + if os.path.isdir(location): + for file in os.listdir(location): + infile = os.path.join(location, file) + if os.path.isfile(infile): + tname, ext = os.path.splitext(file) + if ext == input_ext: + outfile = os.path.join(location, tname + output_ext) + tname = tname.replace(' ', '_').replace('-', '_') + kws = kwargs.copy() + if tname in dct: + kws.update(dct[tname]) + test_name = 'test_%s' % tname + if tname not in exclude: + dct[test_name] = generate_test(infile, outfile, normalize, kws) + else: + dct[test_name] = unittest.skip('Excluded')(lambda: None) + + return type.__new__(cls, name, bases, dct) + + +class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta): + """ + A `unittest.TestCase` subclass for running Markdown's legacy file-based tests. + + A subclass should define various properties which point to a directory of + text-based test files and define various behaviors/defaults for those tests. + The following properties are supported: + + location: A path to the directory of test files. An absolute path is preferred. + exclude: A list of tests to exclude. Each test name should comprise the filename + without an extension. + normalize: A boolean value indicating if the HTML should be normalized. + Default: `False`. + input_ext: A string containing the file extension of input files. Default: `.txt`. + ouput_ext: A string containing the file extension of expected output files. + Default: `html`. + default_kwargs: A `Kwargs` instance which stores the default set of keyword + arguments for all test files in the directory. + + In addition, properties can be defined for each individual set of test files within + the directory. The property should be given the name of the file without the file + extension. Any spaces and dashes in the filename should be replaced with + underscores. The value of the property should be a `Kwargs` instance which + contains the keyword arguments that should be passed to `Markdown` for that + test file. The keyword arguments will "update" the `default_kwargs`. + + When the class instance is created, it will walk the given directory and create + a separate unitttest for each set of test files using the naming scheme: + `test_filename`. One unittest will be run for each set of input and output files. + """ + pass diff --git a/Libs/markdown/treeprocessors.py b/Libs/markdown/treeprocessors.py new file mode 100644 index 0000000..a475fde --- /dev/null +++ b/Libs/markdown/treeprocessors.py @@ -0,0 +1,436 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import xml.etree.ElementTree as etree +from . import util +from . import inlinepatterns + + +def build_treeprocessors(md, **kwargs): + """ Build the default treeprocessors for Markdown. """ + treeprocessors = util.Registry() + treeprocessors.register(InlineProcessor(md), 'inline', 20) + treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10) + return treeprocessors + + +def isString(s): + """ Check if it's string """ + if not isinstance(s, util.AtomicString): + return isinstance(s, str) + return False + + +class Treeprocessor(util.Processor): + """ + Treeprocessors are run on the ElementTree object before serialization. + + Each Treeprocessor implements a "run" method that takes a pointer to an + ElementTree, modifies it as necessary and returns an ElementTree + object. + + Treeprocessors must extend markdown.Treeprocessor. + + """ + def run(self, root): + """ + Subclasses of Treeprocessor should implement a `run` method, which + takes a root ElementTree. This method can return another ElementTree + object, and the existing root ElementTree will be replaced, or it can + modify the current tree and return None. + """ + pass # pragma: no cover + + +class InlineProcessor(Treeprocessor): + """ + A Treeprocessor that traverses a tree, applying inline patterns. + """ + + def __init__(self, md): + self.__placeholder_prefix = util.INLINE_PLACEHOLDER_PREFIX + self.__placeholder_suffix = util.ETX + self.__placeholder_length = 4 + len(self.__placeholder_prefix) \ + + len(self.__placeholder_suffix) + self.__placeholder_re = util.INLINE_PLACEHOLDER_RE + self.md = md + self.inlinePatterns = md.inlinePatterns + self.ancestors = [] + + @property + @util.deprecated("Use 'md' instead.") + def markdown(self): + # TODO: remove this later + return self.md + + def __makePlaceholder(self, type): + """ Generate a placeholder """ + id = "%04d" % len(self.stashed_nodes) + hash = util.INLINE_PLACEHOLDER % id + return hash, id + + def __findPlaceholder(self, data, index): + """ + Extract id from data string, start from index + + Keyword arguments: + + * data: string + * index: index, from which we start search + + Returns: placeholder id and string index, after the found placeholder. + + """ + m = self.__placeholder_re.search(data, index) + if m: + return m.group(1), m.end() + else: + return None, index + 1 + + def __stashNode(self, node, type): + """ Add node to stash """ + placeholder, id = self.__makePlaceholder(type) + self.stashed_nodes[id] = node + return placeholder + + def __handleInline(self, data, patternIndex=0): + """ + Process string with inline patterns and replace it + with placeholders + + Keyword arguments: + + * data: A line of Markdown text + * patternIndex: The index of the inlinePattern to start with + + Returns: String with placeholders. + + """ + if not isinstance(data, util.AtomicString): + startIndex = 0 + count = len(self.inlinePatterns) + while patternIndex < count: + data, matched, startIndex = self.__applyPattern( + self.inlinePatterns[patternIndex], data, patternIndex, startIndex + ) + if not matched: + patternIndex += 1 + return data + + def __processElementText(self, node, subnode, isText=True): + """ + Process placeholders in Element.text or Element.tail + of Elements popped from self.stashed_nodes. + + Keywords arguments: + + * node: parent node + * subnode: processing node + * isText: bool variable, True - it's text, False - it's tail + + Returns: None + + """ + if isText: + text = subnode.text + subnode.text = None + else: + text = subnode.tail + subnode.tail = None + + childResult = self.__processPlaceholders(text, subnode, isText) + + if not isText and node is not subnode: + pos = list(node).index(subnode) + 1 + else: + pos = 0 + + childResult.reverse() + for newChild in childResult: + node.insert(pos, newChild[0]) + + def __processPlaceholders(self, data, parent, isText=True): + """ + Process string with placeholders and generate ElementTree tree. + + Keyword arguments: + + * data: string with placeholders instead of ElementTree elements. + * parent: Element, which contains processing inline data + + Returns: list with ElementTree elements with applied inline patterns. + + """ + def linkText(text): + if text: + if result: + if result[-1][0].tail: + result[-1][0].tail += text + else: + result[-1][0].tail = text + elif not isText: + if parent.tail: + parent.tail += text + else: + parent.tail = text + else: + if parent.text: + parent.text += text + else: + parent.text = text + result = [] + strartIndex = 0 + while data: + index = data.find(self.__placeholder_prefix, strartIndex) + if index != -1: + id, phEndIndex = self.__findPlaceholder(data, index) + + if id in self.stashed_nodes: + node = self.stashed_nodes.get(id) + + if index > 0: + text = data[strartIndex:index] + linkText(text) + + if not isString(node): # it's Element + for child in [node] + list(node): + if child.tail: + if child.tail.strip(): + self.__processElementText( + node, child, False + ) + if child.text: + if child.text.strip(): + self.__processElementText(child, child) + else: # it's just a string + linkText(node) + strartIndex = phEndIndex + continue + + strartIndex = phEndIndex + result.append((node, self.ancestors[:])) + + else: # wrong placeholder + end = index + len(self.__placeholder_prefix) + linkText(data[strartIndex:end]) + strartIndex = end + else: + text = data[strartIndex:] + if isinstance(data, util.AtomicString): + # We don't want to loose the AtomicString + text = util.AtomicString(text) + linkText(text) + data = "" + + return result + + def __applyPattern(self, pattern, data, patternIndex, startIndex=0): + """ + Check if the line fits the pattern, create the necessary + elements, add it to stashed_nodes. + + Keyword arguments: + + * data: the text to be processed + * pattern: the pattern to be checked + * patternIndex: index of current pattern + * startIndex: string index, from which we start searching + + Returns: String with placeholders instead of ElementTree elements. + + """ + new_style = isinstance(pattern, inlinepatterns.InlineProcessor) + + for exclude in pattern.ANCESTOR_EXCLUDES: + if exclude.lower() in self.ancestors: + return data, False, 0 + + if new_style: + match = None + # Since handleMatch may reject our first match, + # we iterate over the buffer looking for matches + # until we can't find any more. + for match in pattern.getCompiledRegExp().finditer(data, startIndex): + node, start, end = pattern.handleMatch(match, data) + if start is None or end is None: + startIndex += match.end(0) + match = None + continue + break + else: # pragma: no cover + match = pattern.getCompiledRegExp().match(data[startIndex:]) + leftData = data[:startIndex] + + if not match: + return data, False, 0 + + if not new_style: # pragma: no cover + node = pattern.handleMatch(match) + start = match.start(0) + end = match.end(0) + + if node is None: + return data, True, end + + if not isString(node): + if not isinstance(node.text, util.AtomicString): + # We need to process current node too + for child in [node] + list(node): + if not isString(node): + if child.text: + self.ancestors.append(child.tag.lower()) + child.text = self.__handleInline( + child.text, patternIndex + 1 + ) + self.ancestors.pop() + if child.tail: + child.tail = self.__handleInline( + child.tail, patternIndex + ) + + placeholder = self.__stashNode(node, pattern.type()) + + if new_style: + return "{}{}{}".format(data[:start], + placeholder, data[end:]), True, 0 + else: # pragma: no cover + return "{}{}{}{}".format(leftData, + match.group(1), + placeholder, match.groups()[-1]), True, 0 + + def __build_ancestors(self, parent, parents): + """Build the ancestor list.""" + ancestors = [] + while parent is not None: + if parent is not None: + ancestors.append(parent.tag.lower()) + parent = self.parent_map.get(parent) + ancestors.reverse() + parents.extend(ancestors) + + def run(self, tree, ancestors=None): + """Apply inline patterns to a parsed Markdown tree. + + Iterate over ElementTree, find elements with inline tag, apply inline + patterns and append newly created Elements to tree. If you don't + want to process your data with inline patterns, instead of normal + string, use subclass AtomicString: + + node.text = markdown.AtomicString("This will not be processed.") + + Arguments: + + * tree: ElementTree object, representing Markdown tree. + * ancestors: List of parent tag names that precede the tree node (if needed). + + Returns: ElementTree object with applied inline patterns. + + """ + self.stashed_nodes = {} + + # Ensure a valid parent list, but copy passed in lists + # to ensure we don't have the user accidentally change it on us. + tree_parents = [] if ancestors is None else ancestors[:] + + self.parent_map = {c: p for p in tree.iter() for c in p} + stack = [(tree, tree_parents)] + + while stack: + currElement, parents = stack.pop() + + self.ancestors = parents + self.__build_ancestors(currElement, self.ancestors) + + insertQueue = [] + for child in currElement: + if child.text and not isinstance( + child.text, util.AtomicString + ): + self.ancestors.append(child.tag.lower()) + text = child.text + child.text = None + lst = self.__processPlaceholders( + self.__handleInline(text), child + ) + for item in lst: + self.parent_map[item[0]] = child + stack += lst + insertQueue.append((child, lst)) + self.ancestors.pop() + if child.tail: + tail = self.__handleInline(child.tail) + dumby = etree.Element('d') + child.tail = None + tailResult = self.__processPlaceholders(tail, dumby, False) + if dumby.tail: + child.tail = dumby.tail + pos = list(currElement).index(child) + 1 + tailResult.reverse() + for newChild in tailResult: + self.parent_map[newChild[0]] = currElement + currElement.insert(pos, newChild[0]) + if len(child): + self.parent_map[child] = currElement + stack.append((child, self.ancestors[:])) + + for element, lst in insertQueue: + for i, obj in enumerate(lst): + newChild = obj[0] + element.insert(i, newChild) + return tree + + +class PrettifyTreeprocessor(Treeprocessor): + """ Add linebreaks to the html document. """ + + def _prettifyETree(self, elem): + """ Recursively add linebreaks to ElementTree children. """ + + i = "\n" + if self.md.is_block_level(elem.tag) and elem.tag not in ['code', 'pre']: + if (not elem.text or not elem.text.strip()) \ + and len(elem) and self.md.is_block_level(elem[0].tag): + elem.text = i + for e in elem: + if self.md.is_block_level(e.tag): + self._prettifyETree(e) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + if not elem.tail or not elem.tail.strip(): + elem.tail = i + + def run(self, root): + """ Add linebreaks to ElementTree root object. """ + + self._prettifyETree(root) + # Do <br />'s separately as they are often in the middle of + # inline content and missed by _prettifyETree. + brs = root.iter('br') + for br in brs: + if not br.tail or not br.tail.strip(): + br.tail = '\n' + else: + br.tail = '\n%s' % br.tail + # Clean up extra empty lines at end of code blocks. + pres = root.iter('pre') + for pre in pres: + if len(pre) and pre[0].tag == 'code': + pre[0].text = util.AtomicString(pre[0].text.rstrip() + '\n') diff --git a/Libs/markdown/util.py b/Libs/markdown/util.py new file mode 100644 index 0000000..46cfe68 --- /dev/null +++ b/Libs/markdown/util.py @@ -0,0 +1,485 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import re +import sys +import warnings +import xml.etree.ElementTree +from collections import namedtuple +from functools import wraps +from itertools import count + +from .pep562 import Pep562 + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + # <PY310 use backport + import importlib_metadata as metadata + +PY37 = (3, 7) <= sys.version_info + + +# TODO: Remove deprecated variables in a future release. +__deprecated__ = { + 'etree': ('xml.etree.ElementTree', xml.etree.ElementTree), + 'string_type': ('str', str), + 'text_type': ('str', str), + 'int2str': ('chr', chr), + 'iterrange': ('range', range) +} + + +""" +Constants you might want to modify +----------------------------------------------------------------------------- +""" + + +BLOCK_LEVEL_ELEMENTS = [ + # Elements which are invalid to wrap in a `<p>` tag. + # See https://w3c.github.io/html/grouping-content.html#the-p-element + 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', + 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', + 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', + 'p', 'pre', 'section', 'table', 'ul', + # Other elements which Markdown should not be mucking up the contents of. + 'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend', + 'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script', + 'style', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video' +] + +# Placeholders +STX = '\u0002' # Use STX ("Start of text") for start-of-placeholder +ETX = '\u0003' # Use ETX ("End of text") for end-of-placeholder +INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:" +INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX +INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)') +AMP_SUBSTITUTE = STX+"amp"+ETX +HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX +HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)') +TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX + + +""" +Constants you probably do not need to change +----------------------------------------------------------------------------- +""" + +# Only load extension entry_points once. +INSTALLED_EXTENSIONS = metadata.entry_points(group='markdown.extensions') +RTL_BIDI_RANGES = ( + ('\u0590', '\u07FF'), + # Hebrew (0590-05FF), Arabic (0600-06FF), + # Syriac (0700-074F), Arabic supplement (0750-077F), + # Thaana (0780-07BF), Nko (07C0-07FF). + ('\u2D30', '\u2D7F') # Tifinagh +) + + +""" +AUXILIARY GLOBAL FUNCTIONS +============================================================================= +""" + + +def deprecated(message, stacklevel=2): + """ + Raise a DeprecationWarning when wrapped function/method is called. + + Usage: + @deprecated("This method will be removed in version X; use Y instead.") + def some_method()" + pass + """ + def wrapper(func): + @wraps(func) + def deprecated_func(*args, **kwargs): + warnings.warn( + f"'{func.__name__}' is deprecated. {message}", + category=DeprecationWarning, + stacklevel=stacklevel + ) + return func(*args, **kwargs) + return deprecated_func + return wrapper + + +@deprecated("Use 'Markdown.is_block_level' instead.") +def isBlockLevel(tag): + """Check if the tag is a block level HTML tag.""" + if isinstance(tag, str): + return tag.lower().rstrip('/') in BLOCK_LEVEL_ELEMENTS + # Some ElementTree tags are not strings, so return False. + return False + + +def parseBoolValue(value, fail_on_errors=True, preserve_none=False): + """Parses a string representing bool value. If parsing was successful, + returns True or False. If preserve_none=True, returns True, False, + or None. If parsing was not successful, raises ValueError, or, if + fail_on_errors=False, returns None.""" + if not isinstance(value, str): + if preserve_none and value is None: + return value + return bool(value) + elif preserve_none and value.lower() == 'none': + return None + elif value.lower() in ('true', 'yes', 'y', 'on', '1'): + return True + elif value.lower() in ('false', 'no', 'n', 'off', '0', 'none'): + return False + elif fail_on_errors: + raise ValueError('Cannot parse bool value: %r' % value) + + +def code_escape(text): + """Escape code.""" + if "&" in text: + text = text.replace("&", "&") + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + return text + + +def _get_stack_depth(size=2): + """Get current stack depth, performantly. + """ + frame = sys._getframe(size) + + for size in count(size): + frame = frame.f_back + if not frame: + return size + + +def nearing_recursion_limit(): + """Return true if current stack depth is within 100 of maximum limit.""" + return sys.getrecursionlimit() - _get_stack_depth() < 100 + + +""" +MISC AUXILIARY CLASSES +============================================================================= +""" + + +class AtomicString(str): + """A string which should not be further processed.""" + pass + + +class Processor: + def __init__(self, md=None): + self.md = md + + @property + @deprecated("Use 'md' instead.") + def markdown(self): + # TODO: remove this later + return self.md + + +class HtmlStash: + """ + This class is used for stashing HTML objects that we extract + in the beginning and replace with place-holders. + """ + + def __init__(self): + """ Create a HtmlStash. """ + self.html_counter = 0 # for counting inline html segments + self.rawHtmlBlocks = [] + self.tag_counter = 0 + self.tag_data = [] # list of dictionaries in the order tags appear + + def store(self, html): + """ + Saves an HTML segment for later reinsertion. Returns a + placeholder string that needs to be inserted into the + document. + + Keyword arguments: + + * html: an html segment + + Returns : a placeholder string + + """ + self.rawHtmlBlocks.append(html) + placeholder = self.get_placeholder(self.html_counter) + self.html_counter += 1 + return placeholder + + def reset(self): + self.html_counter = 0 + self.rawHtmlBlocks = [] + + def get_placeholder(self, key): + return HTML_PLACEHOLDER % key + + def store_tag(self, tag, attrs, left_index, right_index): + """Store tag data and return a placeholder.""" + self.tag_data.append({'tag': tag, 'attrs': attrs, + 'left_index': left_index, + 'right_index': right_index}) + placeholder = TAG_PLACEHOLDER % str(self.tag_counter) + self.tag_counter += 1 # equal to the tag's index in self.tag_data + return placeholder + + +# Used internally by `Registry` for each item in its sorted list. +# Provides an easier to read API when editing the code later. +# For example, `item.name` is more clear than `item[0]`. +_PriorityItem = namedtuple('PriorityItem', ['name', 'priority']) + + +class Registry: + """ + A priority sorted registry. + + A `Registry` instance provides two public methods to alter the data of the + registry: `register` and `deregister`. Use `register` to add items and + `deregister` to remove items. See each method for specifics. + + When registering an item, a "name" and a "priority" must be provided. All + items are automatically sorted by "priority" from highest to lowest. The + "name" is used to remove ("deregister") and get items. + + A `Registry` instance it like a list (which maintains order) when reading + data. You may iterate over the items, get an item and get a count (length) + of all items. You may also check that the registry contains an item. + + When getting an item you may use either the index of the item or the + string-based "name". For example: + + registry = Registry() + registry.register(SomeItem(), 'itemname', 20) + # Get the item by index + item = registry[0] + # Get the item by name + item = registry['itemname'] + + When checking that the registry contains an item, you may use either the + string-based "name", or a reference to the actual item. For example: + + someitem = SomeItem() + registry.register(someitem, 'itemname', 20) + # Contains the name + assert 'itemname' in registry + # Contains the item instance + assert someitem in registry + + The method `get_index_for_name` is also available to obtain the index of + an item using that item's assigned "name". + """ + + def __init__(self): + self._data = {} + self._priority = [] + self._is_sorted = False + + def __contains__(self, item): + if isinstance(item, str): + # Check if an item exists by this name. + return item in self._data.keys() + # Check if this instance exists. + return item in self._data.values() + + def __iter__(self): + self._sort() + return iter([self._data[k] for k, p in self._priority]) + + def __getitem__(self, key): + self._sort() + if isinstance(key, slice): + data = Registry() + for k, p in self._priority[key]: + data.register(self._data[k], k, p) + return data + if isinstance(key, int): + return self._data[self._priority[key].name] + return self._data[key] + + def __len__(self): + return len(self._priority) + + def __repr__(self): + return '<{}({})>'.format(self.__class__.__name__, list(self)) + + def get_index_for_name(self, name): + """ + Return the index of the given name. + """ + if name in self: + self._sort() + return self._priority.index( + [x for x in self._priority if x.name == name][0] + ) + raise ValueError('No item named "{}" exists.'.format(name)) + + def register(self, item, name, priority): + """ + Add an item to the registry with the given name and priority. + + Parameters: + + * `item`: The item being registered. + * `name`: A string used to reference the item. + * `priority`: An integer or float used to sort against all items. + + If an item is registered with a "name" which already exists, the + existing item is replaced with the new item. Treat carefully as the + old item is lost with no way to recover it. The new item will be + sorted according to its priority and will **not** retain the position + of the old item. + """ + if name in self: + # Remove existing item of same name first + self.deregister(name) + self._is_sorted = False + self._data[name] = item + self._priority.append(_PriorityItem(name, priority)) + + def deregister(self, name, strict=True): + """ + Remove an item from the registry. + + Set `strict=False` to fail silently. + """ + try: + index = self.get_index_for_name(name) + del self._priority[index] + del self._data[name] + except ValueError: + if strict: + raise + + def _sort(self): + """ + Sort the registry by priority from highest to lowest. + + This method is called internally and should never be explicitly called. + """ + if not self._is_sorted: + self._priority.sort(key=lambda item: item.priority, reverse=True) + self._is_sorted = True + + # Deprecated Methods which provide a smooth transition from OrderedDict + + def __setitem__(self, key, value): + """ Register item with priority 5 less than lowest existing priority. """ + if isinstance(key, str): + warnings.warn( + 'Using setitem to register a processor or pattern is deprecated. ' + 'Use the `register` method instead.', + DeprecationWarning, + stacklevel=2, + ) + if key in self: + # Key already exists, replace without altering priority + self._data[key] = value + return + if len(self) == 0: + # This is the first item. Set priority to 50. + priority = 50 + else: + self._sort() + priority = self._priority[-1].priority - 5 + self.register(value, key, priority) + else: + raise TypeError + + def __delitem__(self, key): + """ Deregister an item by name. """ + if key in self: + self.deregister(key) + warnings.warn( + 'Using del to remove a processor or pattern is deprecated. ' + 'Use the `deregister` method instead.', + DeprecationWarning, + stacklevel=2, + ) + else: + raise KeyError('Cannot delete key {}, not registered.'.format(key)) + + def add(self, key, value, location): + """ Register a key by location. """ + if len(self) == 0: + # This is the first item. Set priority to 50. + priority = 50 + elif location == '_begin': + self._sort() + # Set priority 5 greater than highest existing priority + priority = self._priority[0].priority + 5 + elif location == '_end': + self._sort() + # Set priority 5 less than lowest existing priority + priority = self._priority[-1].priority - 5 + elif location.startswith('<') or location.startswith('>'): + # Set priority halfway between existing priorities. + i = self.get_index_for_name(location[1:]) + if location.startswith('<'): + after = self._priority[i].priority + if i > 0: + before = self._priority[i-1].priority + else: + # Location is first item` + before = after + 10 + else: + # location.startswith('>') + before = self._priority[i].priority + if i < len(self) - 1: + after = self._priority[i+1].priority + else: + # location is last item + after = before - 10 + priority = before - ((before - after) / 2) + else: + raise ValueError('Not a valid location: "%s". Location key ' + 'must start with a ">" or "<".' % location) + self.register(value, key, priority) + warnings.warn( + 'Using the add method to register a processor or pattern is deprecated. ' + 'Use the `register` method instead.', + DeprecationWarning, + stacklevel=2, + ) + + +def __getattr__(name): + """Get attribute.""" + + deprecated = __deprecated__.get(name) + if deprecated: + warnings.warn( + "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]), + category=DeprecationWarning, + stacklevel=(3 if PY37 else 4) + ) + return deprecated[1] + raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name)) + + +if not PY37: + Pep562(__name__) diff --git a/Pages/index.md b/Pages/index.md new file mode 100755 index 0000000..a73c5eb --- /dev/null +++ b/Pages/index.md @@ -0,0 +1,11 @@ +% - Commento +% Template: Standard.html +% Background: #eeddff + +# Prova +Si testa il semi-generatore di siti statici + +% - Altro commento + +### Sottocoso +Il generatore dovrebbe saper gestire i sottocosi diff --git a/Pages/test/test.md b/Pages/test/test.md new file mode 100644 index 0000000..19b1e62 --- /dev/null +++ b/Pages/test/test.md @@ -0,0 +1 @@ +test. diff --git a/Parts/Standard/BottomBox.html b/Parts/Standard/BottomBox.html new file mode 100644 index 0000000..e69de29 diff --git a/Parts/Standard/LeftBox.html b/Parts/Standard/LeftBox.html new file mode 100644 index 0000000..e69de29 diff --git a/Parts/Standard/TopBox.html b/Parts/Standard/TopBox.html new file mode 100644 index 0000000..e69de29 diff --git a/Templates/Standard.html b/Templates/Standard.html new file mode 100755 index 0000000..efefb4d --- /dev/null +++ b/Templates/Standard.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="Style.css"> + <script src="/App.js"></script> + <title>[HTML:Page:Title] - postoctt + + + +

+
+ [HTML:Part:Standard/TopBox.html] +
+
+
+ [HTML:Part:Standard/LeftBox.html] +
+
+ [HTML:Page:RightBox] +
+
+ [HTML:Page:MainBox] +
+
+
+ [HTML:Part:Standard/BottomBox.html] +
+
+ +