diff options
Diffstat (limited to 'libs/markdown')
24 files changed, 157 insertions, 532 deletions
diff --git a/libs/markdown/__init__.py b/libs/markdown/__init__.py index e05af10da..d88b1e974 100644 --- a/libs/markdown/__init__.py +++ b/libs/markdown/__init__.py @@ -19,43 +19,10 @@ 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 +from .core import Markdown, markdown, markdownFromFile +from .__meta__ import __version__, __version_info__ # noqa # 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/__meta__.py b/libs/markdown/__meta__.py index 37ef099f0..ccabee528 100644 --- a/libs/markdown/__meta__.py +++ b/libs/markdown/__meta__.py @@ -26,7 +26,7 @@ License: BSD (see LICENSE.md for details). # (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, 6, 'final', 0) +__version_info__ = (3, 4, 1, 'final', 0) def _get_version(version_info): diff --git a/libs/markdown/blockparser.py b/libs/markdown/blockparser.py index 39219fdf7..b0ca4b1b5 100644 --- a/libs/markdown/blockparser.py +++ b/libs/markdown/blockparser.py @@ -69,12 +69,6 @@ class BlockParser: 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. diff --git a/libs/markdown/blockprocessors.py b/libs/markdown/blockprocessors.py index dac3f086a..3d0ff86eb 100644 --- a/libs/markdown/blockprocessors.py +++ b/libs/markdown/blockprocessors.py @@ -286,7 +286,7 @@ class BlockQuoteProcessor(BlockProcessor): m = self.RE.search(block) if m: before = block[:m.start()] # Lines before blockquote - # Pass lines before blockquote in recursively for parsing forst. + # Pass lines before blockquote in recursively for parsing first. self.parser.parseBlocks(parent, [before]) # Remove ``> `` from beginning of each line. block = '\n'.join( @@ -321,7 +321,7 @@ class OListProcessor(BlockProcessor): TAG = 'ol' # The integer (python string) with which the lists starts (default=1) - # Eg: If list is intialized as) + # Eg: If list is initialized as) # 3. Item # The ol tag will get starts="3" attribute STARTSWITH = '1' @@ -559,7 +559,7 @@ class EmptyBlockProcessor(BlockProcessor): class ReferenceProcessor(BlockProcessor): """ Process link references. """ RE = re.compile( - r'^[ ]{0,3}\[([^\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE + r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE ) def test(self, parent, block): diff --git a/libs/markdown/core.py b/libs/markdown/core.py index d8c819607..f6a171c11 100644 --- a/libs/markdown/core.py +++ b/libs/markdown/core.py @@ -122,7 +122,7 @@ class Markdown: if isinstance(ext, str): ext = self.build_extension(ext, configs.get(ext, {})) if isinstance(ext, Extension): - ext._extendMarkdown(self) + ext.extendMarkdown(self) logger.debug( 'Successfully loaded extension "%s.%s".' % (ext.__class__.__module__, ext.__class__.__name__) @@ -150,7 +150,7 @@ class Markdown: """ configs = dict(configs) - entry_points = [ep for ep in util.INSTALLED_EXTENSIONS if ep.name == ext_name] + entry_points = [ep for ep in util.get_installed_extensions() if ep.name == ext_name] if entry_points: ext = entry_points[0].load() return ext(**configs) diff --git a/libs/markdown/extensions/__init__.py b/libs/markdown/extensions/__init__.py index 4bc8e5fd7..2d8d72a1e 100644 --- a/libs/markdown/extensions/__init__.py +++ b/libs/markdown/extensions/__init__.py @@ -19,14 +19,13 @@ 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 overriden by a subclass + # Default config -- to be overridden by a subclass # Must be of the following format: # { # 'key': ['value', 'description'] @@ -70,36 +69,16 @@ class Extension: 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 proccesors and patterns to the Markdown Instance. + Add the various processors and patterns to the Markdown Instance. - This method must be overriden by every extension. + 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"' diff --git a/libs/markdown/extensions/codehilite.py b/libs/markdown/extensions/codehilite.py index e1c221816..a54ba21c0 100644 --- a/libs/markdown/extensions/codehilite.py +++ b/libs/markdown/extensions/codehilite.py @@ -23,6 +23,7 @@ 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 + from pygments.util import ClassNotFound pygments = True except ImportError: # pragma: no cover pygments = False @@ -63,12 +64,14 @@ class CodeHilite: * use_pygments: Pass code to pygments for code highlighting. If `False`, the code is instead wrapped for highlighting by a JavaScript library. Default: `True`. + * pygments_formatter: The name of a Pygments formatter or a formatter class used for + highlighting the code blocks. Default: `html`. + * 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-". + * lang_prefix: Prefix prepended to the language. Default: "language-". Other Options: Any other options are accepted and passed on to the lexer and formatter. Therefore, @@ -80,6 +83,10 @@ class CodeHilite: Formatter options: https://pygments.org/docs/formatters/#HtmlFormatter Lexer Options: https://pygments.org/docs/lexers/ + Additionally, when Pygments is enabled, the code's language is passed to the + formatter as an extra option `lang_str`, whose value being `{lang_prefix}{lang}`. + This option has no effect to the Pygments's builtin formatters. + Advanced Usage: code = CodeHilite( src = some_code, @@ -99,6 +106,7 @@ class CodeHilite: self.guess_lang = options.pop('guess_lang', True) self.use_pygments = options.pop('use_pygments', True) self.lang_prefix = options.pop('lang_prefix', 'language-') + self.pygments_formatter = options.pop('pygments_formatter', 'html') if 'linenos' not in options: options['linenos'] = options.pop('linenums', None) @@ -139,7 +147,17 @@ class CodeHilite: 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) + if not self.lang: + # Use the guessed lexer's language instead + self.lang = lexer.aliases[0] + lang_str = f'{self.lang_prefix}{self.lang}' + if isinstance(self.pygments_formatter, str): + try: + formatter = get_formatter_by_name(self.pygments_formatter, **self.options) + except ClassNotFound: + formatter = get_formatter_by_name('html', **self.options) + else: + formatter = self.pygments_formatter(lang_str=lang_str, **self.options) return highlight(self.src, lexer, formatter) else: # just escape and build markup usable by JS highlighting libs @@ -221,7 +239,7 @@ class CodeHilite: class HiliteTreeprocessor(Treeprocessor): - """ Hilight source code in code blocks. """ + """ Highlight source code in code blocks. """ def code_unescape(self, text): """Unescape code.""" @@ -237,11 +255,12 @@ class HiliteTreeprocessor(Treeprocessor): 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=self.config.pop('pygments_style', 'default'), - **self.config + style=local_config.pop('pygments_style', 'default'), + **local_config ) placeholder = self.md.htmlStash.store(code.hilite()) # Clear codeblock in etree instance @@ -253,7 +272,7 @@ class HiliteTreeprocessor(Treeprocessor): class CodeHiliteExtension(Extension): - """ Add source code hilighting to markdown codeblocks. """ + """ Add source code highlighting to markdown codeblocks. """ def __init__(self, **kwargs): # define default configs @@ -278,7 +297,11 @@ class CodeHiliteExtension(Extension): 'lang_prefix': [ 'language-', 'Prefix prepended to the language when use_pygments is false. Default: "language-"' - ] + ], + 'pygments_formatter': ['html', + 'Use a specific formatter for Pygments highlighting.' + 'Default: "html"', + ], } for key, value in kwargs.items(): diff --git a/libs/markdown/extensions/def_list.py b/libs/markdown/extensions/def_list.py index 0e8e45285..17549f031 100644 --- a/libs/markdown/extensions/def_list.py +++ b/libs/markdown/extensions/def_list.py @@ -87,7 +87,7 @@ class DefListProcessor(BlockProcessor): class DefListIndentProcessor(ListIndentProcessor): """ Process indented children of definition list items. """ - # Defintion lists need to be aware of all list types + # Definition lists need to be aware of all list types ITEM_TYPES = ['dd', 'li'] LIST_TYPES = ['dl', 'ol', 'ul'] diff --git a/libs/markdown/extensions/extra.py b/libs/markdown/extensions/extra.py index ebd168c3f..909ba075a 100644 --- a/libs/markdown/extensions/extra.py +++ b/libs/markdown/extensions/extra.py @@ -16,7 +16,7 @@ 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 differant name. You could also edit the `extensions` global +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. diff --git a/libs/markdown/extensions/fenced_code.py b/libs/markdown/extensions/fenced_code.py index 9be0ca056..409166ad8 100644 --- a/libs/markdown/extensions/fenced_code.py +++ b/libs/markdown/extensions/fenced_code.py @@ -22,6 +22,7 @@ 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 @@ -120,30 +121,24 @@ class FencedBlockPreprocessor(Preprocessor): else: id_attr = lang_attr = class_attr = kv_pairs = '' if lang: - lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang) + prefix = self.config.get('lang_prefix', 'language-') + lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"' if classes: - class_attr = ' class="{}"'.format(' '.join(classes)) + class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"' if id: - id_attr = ' id="{}"'.format(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( - '{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments' + kv_pairs = ''.join( + f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments' ) - code = '<pre{id}{cls}><code{lang}{kv}>{code}</code></pre>'.format( - id=id_attr, - cls=class_attr, - lang=lang_attr, - kv=kv_pairs, - code=self._escape(m.group('code')) - ) + 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 = '{}\n{}\n{}'.format(text[:m.start()], - placeholder, - text[m.end():]) + text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}' else: break return text.split("\n") diff --git a/libs/markdown/extensions/footnotes.py b/libs/markdown/extensions/footnotes.py index f6f4c8577..96ed5c25d 100644 --- a/libs/markdown/extensions/footnotes.py +++ b/libs/markdown/extensions/footnotes.py @@ -47,6 +47,10 @@ class FootnoteExtension(Extension): ["↩", "The text string that links from the footnote " "to the reader's place."], + "SUPERSCRIPT_TEXT": + ["{}", + "The text string that links from the reader's place " + "to the footnote."], "BACKLINK_TITLE": ["Jump back to footnote %d in the text", "The text string used for the title HTML attribute " @@ -170,6 +174,9 @@ class FootnoteExtension(Extension): ol = etree.SubElement(div, "ol") surrogate_parent = etree.Element("div") + # Backward compatibility with old '%d' placeholder + backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}") + for index, id in enumerate(self.footnotes.keys(), start=1): li = etree.SubElement(ol, "li") li.set("id", self.makeFootnoteId(id)) @@ -185,7 +192,7 @@ class FootnoteExtension(Extension): backlink.set("class", "footnote-backref") backlink.set( "title", - self.getConfig("BACKLINK_TITLE") % (index) + backlink_title.format(index) ) backlink.text = FN_BACKLINK_TEXT @@ -228,7 +235,7 @@ class FootnoteBlockProcessor(BlockProcessor): # 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 begining of match forward for next iteration. + # 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. @@ -264,7 +271,7 @@ class FootnoteBlockProcessor(BlockProcessor): # 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 begining of match forward for next iteration. + # Add back to blocks everything from beginning of match forward for next iteration. blocks.insert(0, block[m.start():]) # End of this footnote. break @@ -303,7 +310,9 @@ class FootnoteInlineProcessor(InlineProcessor): 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) + a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format( + list(self.footnotes.footnotes.keys()).index(id) + 1 + ) return sup, m.start(0), m.end(0) else: return None, None, None @@ -355,7 +364,7 @@ class FootnotePostTreeprocessor(Treeprocessor): self.offset = 0 for div in root.iter('div'): if div.attrib.get('class', '') == 'footnote': - # Footnotes shoul be under the first orderd list under + # 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) diff --git a/libs/markdown/extensions/legacy_attrs.py b/libs/markdown/extensions/legacy_attrs.py index b51d77807..445aba111 100644 --- a/libs/markdown/extensions/legacy_attrs.py +++ b/libs/markdown/extensions/legacy_attrs.py @@ -26,7 +26,7 @@ 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 compatability. New documents should be authored using attr_lists. However, +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. """ diff --git a/libs/markdown/extensions/legacy_em.py b/libs/markdown/extensions/legacy_em.py index 7fddb77f6..360988b6d 100644 --- a/libs/markdown/extensions/legacy_em.py +++ b/libs/markdown/extensions/legacy_em.py @@ -2,7 +2,7 @@ Legacy Em Extension for Python-Markdown ======================================= -This extention provides legacy behavior for _connected_words_. +This extension provides legacy behavior for _connected_words_. Copyright 2015-2018 The Python Markdown Project diff --git a/libs/markdown/extensions/md_in_html.py b/libs/markdown/extensions/md_in_html.py index 81cc15caa..ec7dcba0e 100644 --- a/libs/markdown/extensions/md_in_html.py +++ b/libs/markdown/extensions/md_in_html.py @@ -248,11 +248,11 @@ class MarkdownInHtmlProcessor(BlockProcessor): def parse_element_content(self, element): """ - Resursively parse the text content of an etree Element as Markdown. + 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 dissabled, the text content of it and its chidlren are wrapped in an `AtomicString`. + been disabled, the text content of it and its chidlren are wrapped in an `AtomicString`. """ md_attr = element.attrib.pop('markdown', 'off') @@ -268,7 +268,7 @@ class MarkdownInHtmlProcessor(BlockProcessor): for child in list(element): self.parse_element_content(child) - # Parse Markdown text in tail of children. Do this seperate to avoid raw HTML parsing. + # 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): @@ -329,7 +329,7 @@ class MarkdownInHtmlProcessor(BlockProcessor): # 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, '') - # Comfirm the match to the blockparser. + # Confirm the match to the blockparser. return True # No match found. return False diff --git a/libs/markdown/extensions/smarty.py b/libs/markdown/extensions/smarty.py index 894805f90..c4bfd58a0 100644 --- a/libs/markdown/extensions/smarty.py +++ b/libs/markdown/extensions/smarty.py @@ -83,7 +83,7 @@ smartypants.py license: from . import Extension from ..inlinepatterns import HtmlInlineProcessor, HTML_RE from ..treeprocessors import InlineProcessor -from ..util import Registry, deprecated +from ..util import Registry # Constants for quote education. @@ -155,12 +155,6 @@ class SubstituteTextPattern(HtmlInlineProcessor): 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: diff --git a/libs/markdown/extensions/tables.py b/libs/markdown/extensions/tables.py index 4b027bb1f..c8b1024a5 100644 --- a/libs/markdown/extensions/tables.py +++ b/libs/markdown/extensions/tables.py @@ -30,9 +30,11 @@ class TableProcessor(BlockProcessor): RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))') RE_END_BORDER = re.compile(r'(?<!\\)(?:\\\\)*\|$') - def __init__(self, parser): + def __init__(self, parser, config): self.border = False self.separator = '' + self.config = config + super().__init__(parser) def test(self, parent, block): @@ -126,7 +128,10 @@ class TableProcessor(BlockProcessor): except IndexError: # pragma: no cover c.text = "" if a: - c.set('align', a) + if self.config['use_align_attribute']: + c.set('align', a) + else: + c.set('style', f'text-align: {a};') def _split_row(self, row): """ split a row of text into list of cells. """ @@ -200,7 +205,7 @@ class TableProcessor(BlockProcessor): if not throw_out: good_pipes.append(pipe) - # Split row according to table delimeters. + # Split row according to table delimiters. pos = 0 for pipe in good_pipes: elements.append(row[pos:pipe]) @@ -212,11 +217,19 @@ class TableProcessor(BlockProcessor): class TableExtension(Extension): """ Add tables to Markdown. """ + def __init__(self, **kwargs): + self.config = { + 'use_align_attribute': [False, 'True to use align attribute instead of style.'], + } + + super().__init__(**kwargs) + 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) + processor = TableProcessor(md.parser, self.getConfigs()) + md.parser.blockprocessors.register(processor, 'table', 75) def makeExtension(**kwargs): # pragma: no cover diff --git a/libs/markdown/extensions/toc.py b/libs/markdown/extensions/toc.py index e4dc3786f..1ded18d63 100644 --- a/libs/markdown/extensions/toc.py +++ b/libs/markdown/extensions/toc.py @@ -16,7 +16,7 @@ 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 +from ..treeprocessors import UnescapeTreeprocessor import re import html import unicodedata @@ -84,8 +84,8 @@ def stashedHTML2text(text, md, strip_entities=True): def unescape(text): """ Unescape escaped text. """ - c = UnescapePostprocessor() - return c.run(text) + c = UnescapeTreeprocessor() + return c.unescape(text) def nest_toc_tokens(toc_list): @@ -160,6 +160,7 @@ class TocTreeprocessor(Treeprocessor): self.base_level = int(config["baselevel"]) - 1 self.slugify = config["slugify"] self.sep = config["separator"] + self.toc_class = config["toc_class"] self.use_anchors = parseBoolValue(config["anchorlink"]) self.anchorlink_class = config["anchorlink_class"] self.use_permalinks = parseBoolValue(config["permalink"], False) @@ -239,7 +240,7 @@ class TocTreeprocessor(Treeprocessor): def build_toc_div(self, toc_list): """ Return a string div given a toc list. """ div = etree.Element("div") - div.attrib["class"] = "toc" + div.attrib["class"] = self.toc_class # Add title to the div if self.title: @@ -288,10 +289,10 @@ class TocTreeprocessor(Treeprocessor): toc_tokens.append({ 'level': int(el.tag[-1]), 'id': el.attrib["id"], - 'name': unescape(stashedHTML2text( + 'name': 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 @@ -328,6 +329,9 @@ class TocExtension(Extension): "title": ["", "Title to insert into TOC <div> - " "Defaults to an empty string"], + "toc_class": ['toc', + 'CSS class(es) used for the link. ' + 'Defaults to "toclink"'], "anchorlink": [False, "True if header should be a self link - " "Defaults to False"], @@ -365,7 +369,7 @@ class TocExtension(Extension): 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 assinged + # 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. diff --git a/libs/markdown/htmlparser.py b/libs/markdown/htmlparser.py index c08856ab8..3512d1a77 100644 --- a/libs/markdown/htmlparser.py +++ b/libs/markdown/htmlparser.py @@ -20,7 +20,7 @@ License: BSD (see LICENSE.md for details). """ import re -import importlib +import importlib.util import sys @@ -113,7 +113,7 @@ class HTMLExtractor(htmlparser.HTMLParser): return m.end() else: # pragma: no cover # Value of self.lineno must exceed total number of lines. - # Find index of begining of last line. + # Find index of beginning of last line. return self.rawdata.rfind('\n') return 0 diff --git a/libs/markdown/inlinepatterns.py b/libs/markdown/inlinepatterns.py index f7d604e74..eb313bd40 100644 --- a/libs/markdown/inlinepatterns.py +++ b/libs/markdown/inlinepatterns.py @@ -211,12 +211,6 @@ class Pattern: # pragma: no cover 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 @@ -673,7 +667,7 @@ class LinkInlineProcessor(InlineProcessor): bracket_count -= 1 elif backtrack_count > 0: backtrack_count -= 1 - # We've found our backup end location if the title doesn't reslove. + # We've found our backup end location if the title doesn't resolve. if backtrack_count == 0: last_bracket = index + 1 diff --git a/libs/markdown/pep562.py b/libs/markdown/pep562.py deleted file mode 100644 index b130d3b1d..000000000 --- a/libs/markdown/pep562.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Backport of PEP 562. - -https://pypi.org/search/?q=pep562 - -Licensed under MIT -Copyright (c) 2018 Isaac Muse <[email protected]> - -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 index f4fb92477..498f7e892 100644 --- a/libs/markdown/postprocessors.py +++ b/libs/markdown/postprocessors.py @@ -37,7 +37,6 @@ def build_postprocessors(md, **kwargs): postprocessors = util.Registry() postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30) postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20) - postprocessors.register(UnescapePostprocessor(), 'unescape', 10) return postprocessors @@ -122,6 +121,10 @@ class AndSubstitutePostprocessor(Postprocessor): return text + "This class will be removed in the future; " + "use 'treeprocessors.UnescapeTreeprocessor' instead." +) class UnescapePostprocessor(Postprocessor): """ Restore escaped chars """ diff --git a/libs/markdown/test_tools.py b/libs/markdown/test_tools.py index 21ae1a7de..2ce0e74f7 100644 --- a/libs/markdown/test_tools.py +++ b/libs/markdown/test_tools.py @@ -42,7 +42,7 @@ class TestCase(unittest.TestCase): 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 ouput and expected ouput are passed + 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. @@ -195,7 +195,7 @@ class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta): text-based test files and define various behaviors/defaults for those tests. The following properties are supported: - location: A path to the directory fo test files. An absolute path is preferred. + 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. diff --git a/libs/markdown/treeprocessors.py b/libs/markdown/treeprocessors.py index eb6bf415e..e9f48ca11 100644 --- a/libs/markdown/treeprocessors.py +++ b/libs/markdown/treeprocessors.py @@ -19,6 +19,7 @@ Copyright 2004 Manfred Stienstra (the original version) License: BSD (see LICENSE.md for details). """ +import re import xml.etree.ElementTree as etree from . import util from . import inlinepatterns @@ -29,6 +30,7 @@ def build_treeprocessors(md, **kwargs): treeprocessors = util.Registry() treeprocessors.register(InlineProcessor(md), 'inline', 20) treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10) + treeprocessors.register(UnescapeTreeprocessor(md), 'unescape', 0) return treeprocessors @@ -75,12 +77,6 @@ class InlineProcessor(Treeprocessor): 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) @@ -331,7 +327,7 @@ class InlineProcessor(Treeprocessor): 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 paterns, instead of normal + want to process your data with inline patterns, instead of normal string, use subclass AtomicString: node.text = markdown.AtomicString("This will not be processed.") @@ -412,8 +408,6 @@ class PrettifyTreeprocessor(Treeprocessor): 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 @@ -433,4 +427,32 @@ class PrettifyTreeprocessor(Treeprocessor): 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') + code = pre[0] + # Only prettify code containing text only + if not len(code) and code.text is not None: + code.text = util.AtomicString(code.text.rstrip() + '\n') + + +class UnescapeTreeprocessor(Treeprocessor): + """ Restore escaped chars """ + + RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX)) + + def _unescape(self, m): + return chr(int(m.group(1))) + + def unescape(self, text): + return self.RE.sub(self._unescape, text) + + def run(self, root): + """ Loop over all elements and unescape all text. """ + for elem in root.iter(): + # Unescape text content + if elem.text and not elem.tag == 'code': + elem.text = self.unescape(elem.text) + # Unescape tail content + if elem.tail: + elem.tail = self.unescape(elem.tail) + # Unescape attribute values + for key, value in elem.items(): + elem.set(key, self.unescape(value)) diff --git a/libs/markdown/util.py b/libs/markdown/util.py index 98cfbf754..e6b08e5ef 100644 --- a/libs/markdown/util.py +++ b/libs/markdown/util.py @@ -22,31 +22,10 @@ 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 functools import wraps, lru_cache 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 @@ -84,8 +63,6 @@ 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), @@ -101,6 +78,16 @@ AUXILIARY GLOBAL FUNCTIONS """ +@lru_cache(maxsize=None) +def get_installed_extensions(): + if sys.version_info >= (3, 10): + from importlib import metadata + else: # <PY310 use backport + import importlib_metadata as metadata + # Only load extension entry_points once. + return metadata.entry_points(group='markdown.extensions') + + def deprecated(message, stacklevel=2): """ Raise a DeprecationWarning when wrapped function/method is called. @@ -123,15 +110,6 @@ def deprecated(message, stacklevel=2): 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, @@ -174,7 +152,7 @@ def _get_stack_depth(size=2): def nearing_recursion_limit(): - """Return true if current stack depth is withing 100 of maximum limit.""" + """Return true if current stack depth is within 100 of maximum limit.""" return sys.getrecursionlimit() - _get_stack_depth() < 100 @@ -193,12 +171,6 @@ 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: """ @@ -349,7 +321,7 @@ class Registry: * `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. Tread carefully as 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. @@ -384,102 +356,3 @@ class Registry: 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 priorty 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__) |