diff --git a/.gitignore b/.gitignore index 6de88e389..456d05c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ target/ .python-version # virtualenv +.venv/ venv/ ENV/ diff --git a/.spell-dict b/.spell-dict index 91cd7bd10..13cdea5b1 100644 --- a/.spell-dict +++ b/.spell-dict @@ -69,6 +69,7 @@ md merchantability metadata MkDocs +mkdocstrings multi MultiMarkdown munge @@ -102,6 +103,7 @@ refactor refactored refactors registerExtension +resettable richeland RSS rST @@ -126,6 +128,7 @@ stdout Stelios Stienstra subclasses +submodules SuperFences svn Swartz diff --git a/docs/changelog.md b/docs/changelog.md index d233abfda..21c464dd2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -27,6 +27,8 @@ links to the header, placing them after all other header content. * Refactor changelog to use the format defined at . * Update the list of empty HTML tags (#1353). * Add customizable TOC title class to TOC extension (#1293). +* Add API documentation of the code base which is generated by + [mkdocstrings](https://mkdocstrings.github.io/) (#1220). ## [3.4.4] -- 2023-07-25 diff --git a/docs/extensions/api.md b/docs/extensions/api.md index d7eb6896d..951496eb8 100644 --- a/docs/extensions/api.md +++ b/docs/extensions/api.md @@ -845,7 +845,7 @@ assert someitem in registry : Add an item to the registry with the given name and priority. - Parameters: + Arguments: * `item`: The item being registered. * `name`: A string used to reference the item. diff --git a/docs/mkdocstrings.css b/docs/mkdocstrings.css new file mode 100644 index 000000000..efc62f485 --- /dev/null +++ b/docs/mkdocstrings.css @@ -0,0 +1,105 @@ +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; +} + +/* Mark external links as such. */ +a.external::after, +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + vertical-align: middle; + position: relative; + + height: 1em; + width: 1em; + background-color: #005b81; +} + +a.external:hover::after, +a.autorefs-external:hover::after { + background-color: #e32e00; +} + +/* Customize custom links. */ +a.autorefs code { + background-color: #ecf0f3; + font-weight: normal; + color: #005B81; +} + +a.autorefs:hover code { + color: #E32E00; +} + +/* Customize labels. */ +.doc-label { + border-radius: 15px; + padding: 2px 8px; + background-color: #f2f2f3; + text-wrap: nowrap; +} + +.doc-label code { + color: #444; +} + +.doc-label-deprecated { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +/* No shadows in any code. */ + +code { + text-shadow: none; +} + +/* Code spans in tables. */ + +table code { + background-color: transparent !important; +} + +/* Style source links */ +a.doc-source-link { + position: relative; + float: right; + border: 1px #005B81 solid; + margin: 0 5px; + padding: 0 5px; + font-weight: normal; + text-shadow: none; + background-color: #f2f2f3; + font-size: 1rem; +} + +a.doc-source-link:hover { + text-decoration: none; + border-color: #E32E00; +} + +/* Customize symbol names. */ +code.doc-symbol-attribute::after { + content: "Attr"; +} + +code.doc-symbol-function::after { + content: "Func"; +} + +code.doc-symbol-method::after { + content: "Method"; +} + +code.doc-symbol-class::after { + content: "Class"; +} + +code.doc-symbol-module::after { + content: "Module"; +} diff --git a/docs/templates/python/nature/attribute.html b/docs/templates/python/nature/attribute.html new file mode 100644 index 000000000..abef8a077 --- /dev/null +++ b/docs/templates/python/nature/attribute.html @@ -0,0 +1,18 @@ +{% extends "_base/attribute.html" %} + +{% block heading scoped %} + ‹› + {%+ filter highlight(language="python", inline=True) %} + {{ attribute_name }}{% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} + {% endfilter %} +{% endblock heading %} + +{% block contents scoped %} + {{ super() }} + {% if attribute.value %} +

Defined Value:

+ {%+ filter highlight(language="python", inline=False) %} +{{ attribute.source }} + {% endfilter %} + {% endif %} +{% endblock contents %} diff --git a/docs/templates/python/nature/class.html b/docs/templates/python/nature/class.html new file mode 100644 index 000000000..7193c3419 --- /dev/null +++ b/docs/templates/python/nature/class.html @@ -0,0 +1,6 @@ +{% extends "_base/class.html" %} + +{% block heading scoped %} + ‹› + {{ super() }} +{% endblock heading %} diff --git a/docs/templates/python/nature/docstring/admonition.html b/docs/templates/python/nature/docstring/admonition.html new file mode 100644 index 000000000..14ec034aa --- /dev/null +++ b/docs/templates/python/nature/docstring/admonition.html @@ -0,0 +1,5 @@ +{{ log.debug("Rendering admonition") }} +
+

{{ section.title|convert_markdown(heading_level, html_id, strip_paragraph=True) }}

+ {{ section.value.contents|convert_markdown(heading_level, html_id) }} +
diff --git a/docs/templates/python/nature/function.html b/docs/templates/python/nature/function.html new file mode 100644 index 000000000..82a696a34 --- /dev/null +++ b/docs/templates/python/nature/function.html @@ -0,0 +1,6 @@ +{% extends "_base/function.html" %} + +{% block heading scoped %} + ‹› + {{ super() }} +{% endblock heading %} diff --git a/docs/templates/python/nature/module.html b/docs/templates/python/nature/module.html new file mode 100644 index 000000000..4d758a388 --- /dev/null +++ b/docs/templates/python/nature/module.html @@ -0,0 +1,6 @@ +{% extends "_base/module.html" %} + +{% block heading scoped %} + ‹› + {{ super() }} +{% endblock heading %} diff --git a/markdown/__init__.py b/markdown/__init__.py index d88b1e974..9674d6e06 100644 --- a/markdown/__init__.py +++ b/markdown/__init__.py @@ -1,23 +1,43 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). -License: BSD (see LICENSE.md for details). """ +Python-Markdown provides two public functions ([`markdown.markdown`][] and [`markdown.markdownFromFile`][]) +both of which wrap the public class [`markdown.Markdown`][]. All submodules support these public functions +and class and/or provide extension support. + +Modules: + core: Core functionality. + preprocessors: Pre-processors. + blockparser: Core Markdown block parser. + blockprocessors: Block processors. + treeprocessors: Tree processors. + inlinepatterns: Inline patterns. + postprocessors: Post-processors. + serializers: Serializers. + util: Utility functions. + htmlparser: HTML parser. + test_tools: Testing utilities. + extensions: Markdown extensions. +""" + +from __future__ import annotations from .core import Markdown, markdown, markdownFromFile from .__meta__ import __version__, __version_info__ # noqa diff --git a/markdown/__main__.py b/markdown/__main__.py index 230ba3435..c323aaac4 100644 --- a/markdown/__main__.py +++ b/markdown/__main__.py @@ -1,23 +1,23 @@ -""" -Python Markdown +# Python Markdown -A Python implementation of John Gruber's 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/ +# 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). +# 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) +# Copyright 2007-2023 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). -""" +# License: BSD (see LICENSE.md for details). + +from __future__ import annotations import sys import optparse diff --git a/markdown/__meta__.py b/markdown/__meta__.py index 7d593844e..082bfa0fa 100644 --- a/markdown/__meta__.py +++ b/markdown/__meta__.py @@ -1,23 +1,21 @@ -""" -Python Markdown +# Python Markdown -A Python implementation of John Gruber's 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/ +# 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). +# 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) +# Copyright 2007-2023 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). -""" +# License: BSD (see LICENSE.md for details). # __version_info__ format: # (major, minor, patch, dev/alpha/beta/rc/final, #) @@ -26,11 +24,15 @@ # (1, 2, 0, 'beta', 2) => "1.2b2" # (1, 2, 0, 'rc', 4) => "1.2rc4" # (1, 2, 0, 'final', 0) => "1.2" + +from __future__ import annotations + + __version_info__ = (3, 4, 4, 'final', 0) def _get_version(version_info): - " Returns a PEP 440-compliant version number from 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') diff --git a/markdown/blockparser.py b/markdown/blockparser.py index 501965ecd..0e5616130 100644 --- a/markdown/blockparser.py +++ b/markdown/blockparser.py @@ -1,27 +1,41 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). + +""" +The block 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. -License: BSD (see LICENSE.md for details). +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. """ +from __future__ import annotations + import xml.etree.ElementTree as etree +from typing import TYPE_CHECKING, Sequence, Any from . import util +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown + class State(list): """ Track the current and nested state of the parser. @@ -41,7 +55,7 @@ class State(list): """ - def set(self, state): + def set(self, state: Any): """ Set a new state. """ self.append(state) @@ -49,7 +63,7 @@ def reset(self): """ Step back one step in nested state. """ self.pop() - def isstate(self, state): + def isstate(self, state: Any) -> bool: """ Test that top (current) level is of given state. """ if len(self): return self[-1] == state @@ -62,30 +76,48 @@ class BlockParser: A wrapper class that stitches the various `BlockProcessors` together, looping through them and creating an `ElementTree` object. + """ - def __init__(self, md): + def __init__(self, md: Markdown): + """ Initialize the block parser. + + Arguments: + md: A Markdown instance. + + Attributes: + BlockParser.md (Markdown): A Markdown instance. + BlockParser.state (State): Tracks the nesting level of current location in document being parsed. + BlockParser.blockprocessors (util.Registry): A collection of + [`blockprocessors`][markdown.blockprocessors]. + + """ self.blockprocessors = util.Registry() self.state = State() self.md = md - def parseDocument(self, lines): - """ Parse a markdown document into an ElementTree. + def parseDocument(self, lines: Sequence[str]) -> etree.ElementTree: + """ 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. + 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. + Arguments: + lines: A list of lines (strings). + + Returns: + An element tree. """ # Create an `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. + def parseChunk(self, parent: etree.Element, text: str): + """ 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 @@ -95,11 +127,15 @@ def parseChunk(self, parent, text): The `parent` `etree` Element passed in is altered in place. Nothing is returned. + Arguments: + parent: The parent element. + text: The text to parse. + """ self.parseBlocks(parent, text.split('\n\n')) - def parseBlocks(self, parent, blocks): - """ Process blocks of markdown text and attach to given `etree` node. + def parseBlocks(self, parent: etree.Element, blocks: Sequence[str]): + """ 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 @@ -110,6 +146,10 @@ def parseBlocks(self, parent, blocks): additional `BlockProcessors` which call this method to recursively parse a nested block. + Arguments: + parent: The parent element. + blocks: The blocks of text to parse. + """ while blocks: for processor in self.blockprocessors: diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index e679e743e..7a3688c2b 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -1,45 +1,45 @@ -""" -Python Markdown - -A Python implementation of John Gruber's Markdown. +# Python Markdown -Documentation: https://python-markdown.github.io/ -GitHub: https://github.com/Python-Markdown/markdown/ -PyPI: https://pypi.org/project/Markdown/ +# A Python implementation of John Gruber's 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). +# Documentation: https://python-markdown.github.io/ +# GitHub: https://github.com/Python-Markdown/markdown/ +# PyPI: https://pypi.org/project/Markdown/ -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) +# 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). -License: BSD (see LICENSE.md for details). +# Copyright 2007-2023 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) -CORE MARKDOWN BLOCKPARSER -=========================================================================== +# License: BSD (see LICENSE.md for details). -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. +""" +A block processor parses blocks of text and adds new elements to the ElementTree. Blocks of text, +separated from other text by blank lines, may have a different syntax and produce a differently +structured tree than other Markdown. Block processors excel at handling code formatting, equation +layouts, tables, etc. """ +from __future__ import annotations + import logging import re import xml.etree.ElementTree as etree +from typing import TYPE_CHECKING, Any from . import util from .blockparser import BlockParser +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown + logger = logging.getLogger('MARKDOWN') -def build_block_parser(md, **kwargs): +def build_block_parser(md: Markdown, **kwargs: Any) -> BlockParser: """ Build the default block parser used by Markdown. """ parser = BlockParser(md) parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100) @@ -65,20 +65,24 @@ class BlockProcessor: whether the current block should be processed by this processor. If the test passes, the parser will call the processors `run` method. + Attributes: + BlockProcessor.parser (BlockParser): The `BlockParser` instance this is attached to. + BlockProcessor.tab_length (int): The tab length set on the `Markdown` instance. + """ - def __init__(self, parser): + def __init__(self, parser: BlockParser): self.parser = parser self.tab_length = parser.md.tab_length - def lastChild(self, parent): + def lastChild(self, parent: etree.Element) -> etree.Element | None: """ Return the last child of an `etree` element. """ if len(parent): return parent[-1] else: return None - def detab(self, text, length=None): + def detab(self, text: str, length: int = None) -> str: """ Remove a tab from the front of each line of the given text. """ if length is None: length = self.tab_length @@ -93,7 +97,7 @@ def detab(self, text, length=None): break return '\n'.join(newtext), '\n'.join(lines[len(newtext):]) - def looseDetab(self, text, level=1): + def looseDetab(self, text: str, level: int = 1) -> str: """ Remove a tab from front of lines but allowing dedented lines. """ lines = text.split('\n') for i in range(len(lines)): @@ -101,7 +105,7 @@ def looseDetab(self, text, level=1): lines[i] = lines[i][self.tab_length*level:] return '\n'.join(lines) - def test(self, parent, block): + def test(self, parent: etree.Element, block: list[str]) -> bool: """ Test for block type. Must be overridden by subclasses. As the parser loops through processors, it will call the `test` @@ -113,35 +117,35 @@ def test(self, parent, block): 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``: An `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. + Keyword arguments: + parent: An `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): + def run(self, parent: etree.Element, blocks: list[str]) -> bool | None: """ 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 + 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 + 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 + to the parent, and should remove (`pop`) or add (`insert`) items to the list of blocks. - Keywords: + If `False` is returned, this will have the same effect as returning `False` + from the `test` method. - * ``parent``: An `etree` element which is the parent of the current block. - * ``blocks``: A list of all remaining blocks of the document. + Keyword arguments: + parent: An `etree` element which is the parent of the current block. + blocks: A list of all remaining blocks of the document. """ pass # pragma: no cover @@ -149,7 +153,8 @@ def run(self, parent, blocks): class ListIndentProcessor(BlockProcessor): """ Process children of list items. - Example: + Example + * a list item process this part @@ -158,7 +163,9 @@ class ListIndentProcessor(BlockProcessor): """ ITEM_TYPES = ['li'] + """ List of tags used for list items. """ LIST_TYPES = ['ul', 'ol'] + """ Types of lists this processor can operate on. """ def __init__(self, *args): super().__init__(*args) @@ -192,7 +199,7 @@ def run(self, parent, blocks): # 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. + # 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` @@ -207,13 +214,13 @@ def run(self, parent, blocks): self.create_item(sibling, block) self.parser.state.reset() - def create_item(self, parent, block): + def create_item(self, parent: etree.Element, block: str): """ 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. """ + def get_level(self, parent: etree.Element, block: str) -> tuple[int, etree.Element]: + """ Get level of indentation based on list level. """ # Get indent level m = self.INDENT_RE.match(block) if m: @@ -275,6 +282,7 @@ def run(self, parent, blocks): class BlockQuoteProcessor(BlockProcessor): + """ Process blockquotes. """ RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)') @@ -288,7 +296,7 @@ def run(self, parent, blocks): 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. + # Remove `> ` from beginning of each line. block = '\n'.join( [self.clean(line) for line in block[m.start():].split('\n')] ) @@ -305,8 +313,8 @@ def run(self, parent, blocks): self.parser.parseChunk(quote, block) self.parser.state.reset() - def clean(self, line): - """ Remove ``>`` from beginning of a line. """ + def clean(self, line: str) -> str: + """ Remove `>` from beginning of a line. """ m = self.RE.match(line) if line.strip() == ">": return "" @@ -319,19 +327,24 @@ def clean(self, line): class OListProcessor(BlockProcessor): """ Process ordered list blocks. """ - TAG = 'ol' - # The integer (python string) with which the lists starts (default=1) - # Example: 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'] + TAG: str = 'ol' + """ The tag used for the the wrapping element. """ + STARTSWITH: int = '1' + """ + The integer (as a string ) with which the list starts. For example, if a list is initialized as + `3. Item`, then the `ol` tag will be assigned an HTML attribute of `starts="3"`. Default: `"1"`. + """ + LAZY_OL: bool = True + """ Ignore `STARTSWITH` if `True`. """ + SIBLING_TAGS: list[str] = ['ol', 'ul'] + """ + Markdown does not require the type of a new list item match the previous list item type. + This is the list of types which can be mixed. + """ def __init__(self, parser): super().__init__(parser) - # Detect an item (``1. item``). ``group(1)`` contains contents of item. + # 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+\.)|[*+-])[ ]+(.*)' % @@ -402,7 +415,7 @@ def run(self, parent, blocks): self.parser.parseBlocks(li, [item]) self.parser.state.reset() - def get_items(self, block): + def get_items(self, block: str) -> list[str]: """ Break a block into list items. """ items = [] for line in block.split('\n'): @@ -432,11 +445,12 @@ def get_items(self, block): class UListProcessor(OListProcessor): """ Process unordered list blocks. """ - TAG = 'ul' + TAG: str = 'ul' + """ The tag used for the the wrapping element. """ def __init__(self, parser): super().__init__(parser) - # Detect an item (``1. item``). ``group(1)`` contains contents of item. + # Detect an item (`1. item`). `group(1)` contains contents of item. self.RE = re.compile(r'^[ ]{0,%d}[*+-][ ]+(.*)' % (self.tab_length - 1)) @@ -482,7 +496,7 @@ def test(self, parent, block): def run(self, parent, blocks): lines = blocks.pop(0).split('\n') - # Determine level. ``=`` is 1 and ``-`` is 2. + # Determine level. `=` is 1 and `-` is 2. if lines[1].startswith('='): level = 1 else: diff --git a/markdown/core.py b/markdown/core.py index 77cd2d7ad..51fe8ee88 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -1,28 +1,29 @@ -""" -Python Markdown +# Python Markdown -A Python implementation of John Gruber's 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/ +# 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). +# 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) +# Copyright 2007-2023 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). -""" +# License: BSD (see LICENSE.md for details). + +from __future__ import annotations import codecs import sys import logging import importlib +from typing import TYPE_CHECKING, Any, TextIO, Callable from . import util from .preprocessors import build_preprocessors from .blockprocessors import build_block_parser @@ -33,6 +34,9 @@ from .serializers import to_html_string, to_xhtml_string from .util import BLOCK_LEVEL_ELEMENTS +if TYPE_CHECKING: # pragma: no cover + from xml.etree.ElementTree import Element + __all__ = ['Markdown', 'markdown', 'markdownFromFile'] @@ -40,56 +44,111 @@ class Markdown: - """Convert Markdown to HTML.""" + """ + A parser which converts Markdown to HTML. + + Attributes: + Markdown.tab_length (int): The number of spaces which correspond to a single tab. Default: `4`. + Markdown.ESCAPED_CHARS (list[str]): List of characters which get the backslash escape treatment. + Markdown.block_level_elements (list[str]): List of HTML tags which get treated as block-level elements. + See [`markdown.util.BLOCK_LEVEL_ELEMENTS`][] for the full list of elements. + Markdown.registeredExtensions (list[Extension]): List of extensions which have called + [`registerExtension`][markdown.Markdown.registerExtension] during setup. + Markdown.doc_tag (str): Element used to wrap document. Default: `div`. + Markdown.stripTopLevelTags (bool): Indicates whether the `doc_tag` should be removed. Default: 'True'. + Markdown.references (dict[str, tuple[str, str]]): A mapping of link references found in a parsed document + where the key is the reference name and the value is a tuple of the URL and title. + Markdown.htmlStash (util.HtmlStash): The instance of the `HtmlStash` used by an instance of this class. + Markdown.output_formats (dict[str, Callable[xml.etree.ElementTree.Element]]): A mapping of known output + formats by name and their respective serializers. Each serializer must be a callable which accepts an + [`Element`][xml.etree.ElementTree.Element] and returns a `str`. + Markdown.output_format (str): The output format set by + [`set_output_format`][markdown.Markdown.set_output_format]. + Markdown.serializer (Callable[xml.etree.ElementTree.Element]): The serializer set by + [`set_output_format`][markdown.Markdown.set_output_format]. + Markdown.preprocessors (util.Registry): A collection of [`preprocessors`][markdown.preprocessors]. + Markdown.parser (blockparser.BlockParser): A collection of [`blockprocessors`][markdown.blockprocessors]. + Markdown.inlinePatterns (util.Registry): A collection of [`inlinepatterns`][markdown.inlinepatterns]. + Markdown.treeprocessors (util.Registry): A collection of [`treeprocessors`][markdown.treeprocessors]. + Markdown.postprocessors (util.Registry): A collection of [`postprocessors`][markdown.postprocessors]. + + """ doc_tag = "div" # Element used to wrap document - later removed - output_formats = { + output_formats: dict[str, Callable[Element]] = { 'html': to_html_string, 'xhtml': to_xhtml_string, } + """ + A mapping of known output formats by name and their respective serializers. Each serializer must be a + callable which accepts an [`Element`][xml.etree.ElementTree.Element] and returns a `str`. + """ def __init__(self, **kwargs): """ Creates a new Markdown instance. - Keyword arguments: + Keyword Arguments: + extensions (list[Extension | str]): A list of extensions. + + If an item is an instance of a subclass of [`markdown.extensions.Extension`][], + the instance will be used as-is. If an item is of type `str`, it is passed + to [`build_extension`][markdown.Markdown.build_extension] with its corresponding + `extension_configs` and the returned instance of [`markdown.extensions.Extension`][] + is used. + extension_configs (dict[str, dict[str, Any]]): Configuration settings for extensions. + output_format (str): Format of output. Supported formats are: - * `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 + * `xhtml`: Outputs XHTML style tags. Default. + * `html`: Outputs HTML style tags. + tab_length (int): Length of tabs in the source. Default: `4` """ - self.tab_length = kwargs.get('tab_length', 4) + self.tab_length: int = kwargs.get('tab_length', 4) - self.ESCAPED_CHARS = ['\\', '`', '*', '_', '{', '}', '[', ']', - '(', ')', '>', '#', '+', '-', '.', '!'] + self.ESCAPED_CHARS: list[str] = [ + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!' + ] + """ List of characters which get the backslash escape treatment. """ - self.block_level_elements = BLOCK_LEVEL_ELEMENTS.copy() + self.block_level_elements: list[str] = BLOCK_LEVEL_ELEMENTS.copy() - self.registeredExtensions = [] - self.docType = "" - self.stripTopLevelTags = True + self.registeredExtensions: list[Extension] = [] + self.docType = "" # TODO: Maybe delete this. It does not appear to be used anymore. + self.stripTopLevelTags: bool = True self.build_parser() - self.references = {} - self.htmlStash = util.HtmlStash() + self.references: dict[str, tuple[str, str]] = {} + self.htmlStash: util.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. """ + def build_parser(self) -> Markdown: + """ + Build the parser from the various parts. + + Assigns a value to each of the following attributes on the class instance: + + * **`Markdown.preprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of + [`preprocessors`][markdown.preprocessors]. + * **`Markdown.parser`** ([`BlockParser`][markdown.blockparser.BlockParser]) -- A collection of + [`blockprocessors`][markdown.blockprocessors]. + * **`Markdown.inlinePatterns`** ([`Registry`][markdown.util.Registry]) -- A collection of + [`inlinepatterns`][markdown.inlinepatterns]. + * **`Markdown.treeprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of + [`treeprocessors`][markdown.treeprocessors]. + * **`Markdown.postprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of + [`postprocessors`][markdown.postprocessors]. + + This method could be redefined in a subclass to build a custom parser which is made up of a different + combination of processors and patterns. + + """ self.preprocessors = build_preprocessors(self) self.parser = build_block_parser(self) self.inlinePatterns = build_inlinepatterns(self) @@ -97,15 +156,18 @@ def build_parser(self): self.postprocessors = build_postprocessors(self) return self - def registerExtensions(self, extensions, configs): + def registerExtensions(self, extensions: list[Extension | str], configs: dict[str, dict[str, Any]]) -> Markdown: """ - Register extensions with this instance of Markdown. + Load a list of extensions into an instance of the `Markdown` class. - Keyword arguments: + Arguments: + extensions (list[Extension | str]): A list of extensions. - * `extensions`: A list of extensions, which can either - be strings or objects. - * `configs`: A dictionary mapping extension names to `configs` options. + If an item is an instance of a subclass of [`markdown.extensions.Extension`][], + the instance will be used as-is. If an item is of type `str`, it is passed + to [`build_extension`][markdown.Markdown.build_extension] with its corresponding `configs` and the + returned instance of [`markdown.extensions.Extension`][] is used. + configs (dict[str, dict[str, Any]]): Configuration settings for extensions. """ for ext in extensions: @@ -126,17 +188,24 @@ def registerExtensions(self, extensions, configs): ) return self - def build_extension(self, ext_name, configs): + def build_extension(self, ext_name: str, configs: dict[str, Any]) -> Extension: """ - Build extension from a string name, then return an instance. + Build extension from a string name, then return an instance using the given `configs`. + + Arguments: + ext_name: Name of extension as a string. + configs: Configuration settings for extension. + + Returns: + An instance of the extension with the given configuration settings. 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. + `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. + the [`markdown.extensions.Extension`][] instance returned by that function. """ configs = dict(configs) @@ -172,14 +241,27 @@ def build_extension(self, ext_name, configs): e.args = (message,) + e.args[1:] raise - def registerExtension(self, extension): - """ This gets called by the extension """ + def registerExtension(self, extension: Extension) -> Markdown: + """ + Register an extension as having a resettable state. + + Arguments: + extension: An instance of the extension to register. + + This should get called once by an extension during setup. A "registered" extension's + `reset` method is called by [`Markdown.reset()`][markdown.Markdown.reset]. Not all extensions have or need a + resettable state, and so it should not be assumed that all extensions are "registered." + + """ self.registeredExtensions.append(extension) return self - def reset(self): + def reset(self) -> Markdown: """ - Resets all state variables so that we can start with a new text. + Resets all state variables to prepare the parser instance for new input. + + Called once upon creation of a class instance. Should be called manually between calls + to [`Markdown.convert`][markdown.Markdown.convert]. """ self.htmlStash.reset() self.references.clear() @@ -190,8 +272,14 @@ def reset(self): return self - def set_output_format(self, format): - """ Set the output format for the class instance. """ + def set_output_format(self, format: str) -> Markdown: + """ + Set the output format for the class instance. + + Arguments: + format: Must be a known value in `Markdown.output_formats`. + + """ self.output_format = format.lower().rstrip('145') # ignore number try: self.serializer = self.output_formats[self.output_format] @@ -205,32 +293,43 @@ def set_output_format(self, format): raise return self - def is_block_level(self, tag): - """Check if the tag is a block level HTML tag.""" + # Note: the `tag` argument is type annotated `Any` as ElementTree uses many various objects as tags. + # As there is no standardization in ElementTree, the type of a given tag is unpredictable. + def is_block_level(self, tag: Any) -> bool: + """ + Check if the given `tag` is a block level HTML tag. + + Returns `True` for any string listed in `Markdown.block_level_elements`. A `tag` which is + not a string always returns `False`. + + """ 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): + def convert(self, source: str) -> str: """ - Convert markdown to serialized XHTML or HTML. + Convert a Markdown string to a string in the specified output format. - Keyword arguments: + Arguments: + source: Markdown formatted text as Unicode or ASCII string. - * `source`: Source text as a Unicode string. + Returns: + A string in the specified output format. - Markdown processing takes place in five steps: + Markdown parsing 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. + 1. A bunch of [`preprocessors`][markdown.preprocessors] munge the input text. + 2. A [`BlockParser`][markdown.blockparser.BlockParser] parses the high-level structural elements of the + pre-processed text into an [`ElementTree`][xml.etree.ElementTree.ElementTree] object. + 3. A bunch of [`treeprocessors`][markdown.treeprocessors] are run against the + [`ElementTree`][xml.etree.ElementTree.ElementTree] object. One such `treeprocessor` + ([`markdown.treeprocessors.InlineProcessor`][]) runs [`inlinepatterns`][markdown.inlinepatterns] + against the [`ElementTree`][xml.etree.ElementTree.ElementTree] object, parsing inline markup. + 4. Some [`postprocessors`][markdown.postprocessors] are run against the text after the + [`ElementTree`][xml.etree.ElementTree.ElementTree] object has been serialized into text. + 5. The output is returned as a string. """ @@ -282,24 +381,30 @@ def convert(self, source): return output.strip() - def convertFile(self, input=None, output=None, encoding=None): - """Converts a markdown file and returns the HTML as a Unicode string. + def convertFile( + self, + input: str | TextIO | None = None, + output: str | TextIO | None = None, + encoding: str | None = None, + ) -> Markdown: + """ + 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 + 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. + encoding as the source file. The + [`xmlcharrefreplace`](https://docs.python.org/3/library/codecs.html#error-handlers) + 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`. + 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`. """ @@ -358,37 +463,41 @@ def convertFile(self, input=None, output=None, encoding=None): """ -def markdown(text, **kwargs): - """Convert a markdown string to HTML and return HTML as a Unicode string. +def markdown(text: str, **kwargs: Any) -> str: + """ + 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 + This is a shortcut function for [`Markdown`][markdown.Markdown] class to cover the most + basic use case. It initializes an instance of [`Markdown`][markdown.Markdown], loads the necessary extensions and runs the parser on the given text. - Keyword arguments: + Arguments: + text: Markdown formatted text as Unicode or ASCII string. - * `text`: Markdown formatted text as Unicode or ASCII string. - * Any arguments accepted by the Markdown class. + Keyword arguments: + **kwargs: Any arguments accepted by the Markdown class. - Returns: An HTML document as a string. + Returns: + A string in the specified output format. """ 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. +def markdownFromFile(**kwargs: Any): + """ + Read Markdown text from a file and write output 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`. + This is a shortcut function which initializes an instance of [`Markdown`][markdown.Markdown], + and calls the [`convertFile`][markdown.Markdown.convertFile] method rather than + [`convert`][markdown.Markdown.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. + input (str | TextIO): A file name or readable object. + output (str | TextIO): A file name or writable object. + encoding (str): Encoding of input and output. + **kwargs: Any arguments accepted by the `Markdown` class. """ md = Markdown(**kwargs) diff --git a/markdown/extensions/__init__.py b/markdown/extensions/__init__.py index 7a32e1c52..36baf1f75 100644 --- a/markdown/extensions/__init__.py +++ b/markdown/extensions/__init__.py @@ -1,83 +1,142 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). -License: BSD (see LICENSE.md for details). """ +Markdown accepts an [`Extension`][markdown.extensions.Extension] instance for each extension. Therefore, each extension +must to define a class that extends [`Extension`][markdown.extensions.Extension] and over-rides the +[`extendMarkdown`][markdown.extensions.Extension.extendMarkdown] method. Within this class one can manage configuration +options for their extension and attach the various processors and patterns which make up an extension to the +[`Markdown`][markdown.Markdown] instance. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Any from ..util import parseBoolValue +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown + class Extension: """ Base class for extensions to subclass. """ - # Default configuration -- 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 = {} + config: dict[str, list[Any, str]] = {} + """ + Default configuration for an extension. + + This attribute is to be defined in a subclass and must be of the following format: + + ``` python + config = { + 'key': ['value', 'description'] + } + ``` + + Note that [`setConfig`][markdown.extensions.Extension.setConfig] will raise a [`KeyError`][] + if a default is not set for each option. + """ 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. """ + def getConfig(self, key: str, default: Any = '') -> Any: + """ + Return a single configuration option value. + + Arguments: + key: The configuration option name. + default: Default value to return if key is not set. + + Returns: + Value of stored configuration option. + """ if key in self.config: return self.config[key][0] else: return default - def getConfigs(self): - """ Return all configs settings as a dict. """ + def getConfigs(self) -> dict[str, Any]: + """ + Return all configuration options. + + Returns: + All configuration options. + """ return {key: self.getConfig(key) for key in self.config.keys()} - def getConfigInfo(self): - """ Return all `config` descriptions as a list of tuples. """ + def getConfigInfo(self) -> list[tuple[str, str]]: + """ + Return descriptions of all configuration options. + + Returns: + All descriptions of configuration options. + """ 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`. """ + def setConfig(self, key: str, value: Any): + """ + Set a configuration option. + + If the corresponding default value set in [`config`][markdown.extensions.Extension.config] + is a `bool` value or `None`, then `value` is passed through + [`parseBoolValue`][markdown.util.parseBoolValue] before being stored. + + Arguments: + key: Name of configuration option to set. + value: Value to assign to option. + + Raises: + KeyError: If `key` is not known. + """ 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. """ + def setConfigs(self, items: dict[str, Any] | list[tuple[str, Any]]): + """ + Loop through a collection of configuration options, passing each to + [`setConfig`][markdown.extensions.Extension.setConfig]. + + Arguments: + items: Collection of configuration options. + + Raises: + KeyError: for any unknown key. + """ if hasattr(items, 'items'): # it's a dict items = items.items() for key, value in items: self.setConfig(key, value) - def extendMarkdown(self, md): + def extendMarkdown(self, md: Markdown): """ Add the various processors and patterns to the Markdown Instance. This method must be overridden by every extension. - Keyword arguments: - - * md: The Markdown instance. + Arguments: + md: The Markdown instance. """ raise NotImplementedError( diff --git a/markdown/extensions/abbr.py b/markdown/extensions/abbr.py index 064987047..c060f4757 100644 --- a/markdown/extensions/abbr.py +++ b/markdown/extensions/abbr.py @@ -1,20 +1,26 @@ -''' -Abbreviation Extension for Python-Markdown -========================================== +# Abbreviation Extension for Python-Markdown +# ========================================== -This extension adds abbreviation handling to Python-Markdown. +# This extension adds abbreviation handling to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/abbreviations +# for documentation. -See -for documentation. +# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/) +# and [Seemant Kulleen](http://www.kulleen.org/) -Original 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 -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" +This extension adds abbreviation handling to Python-Markdown. -License: [BSD](https://opensource.org/licenses/bsd-license.php) +See the [documentation](https://Python-Markdown.github.io/extensions/abbreviations) +for details. +""" -''' +from __future__ import annotations from . import Extension from ..blockprocessors import BlockProcessor @@ -41,11 +47,11 @@ 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: @@ -66,7 +72,7 @@ def run(self, parent, blocks): return False def _generate_pattern(self, text): - ''' + """ Given a string, returns an regex pattern to match that string. 'HTML' -> r'(?P[H][T][M][L])' @@ -74,7 +80,7 @@ def _generate_pattern(self, text): 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] diff --git a/markdown/extensions/admonition.py b/markdown/extensions/admonition.py index ce8492f4a..d5d273d7b 100644 --- a/markdown/extensions/admonition.py +++ b/markdown/extensions/admonition.py @@ -1,22 +1,31 @@ -""" -Admonition extension for Python-Markdown -======================================== +# Admonition extension for Python-Markdown +# ======================================== -Adds rST-style admonitions. Inspired by [rST][] feature with the same name. +# Adds rST-style admonitions. Inspired by [rST][] feature with the same name. -[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions +# [rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions + +# See https://Python-Markdown.github.io/extensions/admonition +# for documentation. + +# Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/). -See -for documentation. +# All changes Copyright The Python Markdown Project -Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/). +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -All changes Copyright The Python Markdown Project -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Adds rST-style admonitions. Inspired by [rST][] feature with the same name. +[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions + +See the [documentation](https://Python-Markdown.github.io/extensions/admonition) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import BlockProcessor import xml.etree.ElementTree as etree diff --git a/markdown/extensions/attr_list.py b/markdown/extensions/attr_list.py index bc06786d3..f430ec300 100644 --- a/markdown/extensions/attr_list.py +++ b/markdown/extensions/attr_list.py @@ -1,26 +1,38 @@ -""" -Attribute List Extension for Python-Markdown -============================================ +# 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. +# Adds attribute list syntax. Inspired by +# [Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s +# feature of the same name. -See -for documentation. +# See https://Python-Markdown.github.io/extensions/attr_list +# for documentation. -Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/). +# Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/). -All changes Copyright 2011-2014 The Python Markdown Project +# All changes Copyright 2011-2014 The Python Markdown Project -License: [BSD](https://opensource.org/licenses/bsd-license.php) +# License: [BSD](https://opensource.org/licenses/bsd-license.php) """ + Adds attribute list syntax. Inspired by +[Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s +feature of the same name. + +See the [documentation](https://Python-Markdown.github.io/extensions/attr_list) +for details. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING from . import Extension from ..treeprocessors import Treeprocessor import re +if TYPE_CHECKING: # pragma: no cover + from xml.etree.ElementTree import Element + def _handle_double_quote(s, t): k, v = t.split('=', 1) @@ -53,12 +65,12 @@ def _handle_word(s, t): ]) -def get_attrs(str): +def get_attrs(str: str) -> list[tuple[str, str]]: """ Parse attribute list and return a list of attribute tuples. """ return _scanner.scan(str)[0] -def isheader(elem): +def isheader(elem: Element) -> bool: return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] @@ -74,7 +86,7 @@ class AttrListTreeprocessor(Treeprocessor): r'\uf900-\ufdcf\ufdf0-\ufffd' r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+') - def run(self, doc): + def run(self, doc: Element): for elem in doc.iter(): if self.md.is_block_level(elem.tag): # Block level: check for `attrs` on last line of text @@ -134,7 +146,7 @@ def run(self, doc): self.assign_attrs(elem, m.group(1)) elem.tail = elem.tail[m.end():] - def assign_attrs(self, elem, attrs): + def assign_attrs(self, elem: Element, attrs: dict[str, str]): """ Assign `attrs` to element. """ for k, v in get_attrs(attrs): if k == '.': @@ -148,7 +160,7 @@ def assign_attrs(self, elem, attrs): # assign attribute `k` with `v` elem.set(self.sanitize_name(k), v) - def sanitize_name(self, name): + def sanitize_name(self, name: str) -> str: """ Sanitize name as 'an XML Name, minus the ":"'. See https://www.w3.org/TR/REC-xml-names/#NT-NCName @@ -157,6 +169,7 @@ def sanitize_name(self, name): class AttrListExtension(Extension): + """ Attribute List extension for Python-Markdown """ def extendMarkdown(self, md): md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8) md.registerExtension(self) diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py index 1941ce288..f8d25b0fa 100644 --- a/markdown/extensions/codehilite.py +++ b/markdown/extensions/codehilite.py @@ -1,20 +1,26 @@ -""" -CodeHilite Extension for Python-Markdown -======================================== +# CodeHilite Extension for Python-Markdown +# ======================================== -Adds code/syntax highlighting to standard Python-Markdown code blocks. +# Adds code/syntax highlighting to standard Python-Markdown code blocks. + +# See https://Python-Markdown.github.io/extensions/code_hilite +# for documentation. -See -for documentation. +# Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/). -Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/). +# All changes Copyright 2008-2014 The Python Markdown Project -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Adds code/syntax highlighting to standard Python-Markdown code blocks. +See the [documentation](https://Python-Markdown.github.io/extensions/code_hilite) +for details. """ +from __future__ import annotations + from . import Extension from ..treeprocessors import Treeprocessor from ..util import parseBoolValue @@ -29,11 +35,11 @@ pygments = False -def parse_hl_lines(expr): +def parse_hl_lines(expr: str) -> list[int]: """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. + `expr` should be like '1 2' to emphasize lines 1 and 2 of a code block. + Returns a list of integers, the line numbers to emphasize. """ if not expr: return [] @@ -50,57 +56,59 @@ 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`. - - * `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`. + ```python + code = CodeHilite(src=some_code, lang='python') + html = code.hilite() + ``` - * `css_class`: An alias to Pygments `cssclass` formatter option. Default: 'codehilite'. - - * `lang_prefix`: Prefix prepended to the language. Default: "language-". + Arguments: + src: Source string or any object with a `.readline` attribute. + + Keyword arguments: + lang (str): String name of Pygments lexer to use for highlighting. Default: `None`. + guess_lang (bool): Auto-detect which lexer to use. + Ignored if `lang` is set to a valid value. Default: `True`. + use_pygments (bool): Pass code to Pygments for code highlighting. If `False`, the code is + instead wrapped for highlighting by a JavaScript library. Default: `True`. + pygments_formatter (str): The name of a Pygments formatter or a formatter class used for + highlighting the code blocks. Default: `html`. + linenums (bool): An alias to Pygments `linenos` formatter option. Default: `None`. + css_class (str): An alias to Pygments `cssclass` formatter option. Default: 'codehilite'. + lang_prefix (str): Prefix prepended to the language. 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/ + * **Formatter options**: + * **Lexer Options**: 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' builtin formatters. Advanced Usage: - code = CodeHilite( - src = some_code, - lang = 'php', - startinline = True, # Lexer option. Snippet does not start with ` str: """ Pass code to the [Pygments](https://pygments.org/) highlighter with optional line numbers. The output should then be styled with CSS to @@ -277,32 +285,32 @@ class CodeHiliteExtension(Extension): 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
- " - "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'], + 'linenums': [ + None, "Use lines numbers. True|table|inline=yes, False=no, None=auto. Default: `None`." + ], + 'guess_lang': [ + True, "Automatic language detection - Default: `True`." + ], + 'css_class': [ + "codehilite", "Set class name for wrapper
- 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, 'Highlight code blocks with pygments. Disable if using a JavaScript library. Default: `True`.' + ], 'lang_prefix': [ - 'language-', - 'Prefix prepended to the language when use_pygments is false. Default: "language-"' + '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`.' ], - 'pygments_formatter': ['html', - 'Use a specific formatter for Pygments highlighting.' - 'Default: "html"', - ], - } + } + """ Default configuration options. """ for key, value in kwargs.items(): if key in self.config: diff --git a/markdown/extensions/def_list.py b/markdown/extensions/def_list.py index c2f23a694..54273b602 100644 --- a/markdown/extensions/def_list.py +++ b/markdown/extensions/def_list.py @@ -1,20 +1,26 @@ -""" -Definition List Extension for Python-Markdown -============================================= +# Definition List Extension for Python-Markdown +# ============================================= -Adds parsing of Definition Lists to Python-Markdown. +# Adds parsing of Definition Lists to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/definition_lists +# for documentation. -See -for documentation. +# Original code Copyright 2008 [Waylan Limberg](http://achinghead.com) -Original code Copyright 2008 [Waylan Limberg](http://achinghead.com) +# All changes Copyright 2008-2014 The Python Markdown Project -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Adds parsing of Definition Lists to Python-Markdown. +See the [documentation](https://Python-Markdown.github.io/extensions/definition_lists) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import BlockProcessor, ListIndentProcessor import xml.etree.ElementTree as etree @@ -89,7 +95,9 @@ class DefListIndentProcessor(ListIndentProcessor): # Definition lists need to be aware of all list types ITEM_TYPES = ['dd', 'li'] + """ Include `dd` in list item types. """ LIST_TYPES = ['dl', 'ol', 'ul'] + """ Include `dl` is list types. """ def create_item(self, parent, block): """ Create a new `dd` or `li` (depending on parent) and parse the block with it as the parent. """ diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py index 8f73806e1..74ebc192c 100644 --- a/markdown/extensions/extra.py +++ b/markdown/extensions/extra.py @@ -1,12 +1,22 @@ -""" -Python-Markdown Extra Extension -=============================== +# Python-Markdown Extra Extension +# =============================== + +# A compilation of various Python-Markdown extensions that imitates +# [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/). + +# See https://Python-Markdown.github.io/extensions/extra +# for documentation. + +# Copyright The Python Markdown Project + +# License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" 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 +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. @@ -20,15 +30,12 @@ variable defined below, but be aware that such changes may be lost when you upgrade to any future version of Python-Markdown. -See -for documentation. - -Copyright The Python Markdown Project - -License: [BSD](https://opensource.org/licenses/bsd-license.php) - +See the [documentation](https://Python-Markdown.github.io/extensions/extra) +for details. """ +from __future__ import annotations + from . import Extension extensions = [ @@ -40,6 +47,7 @@ 'abbr', 'md_in_html' ] +""" The list of included extensions. """ class ExtraExtension(Extension): diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py index a0874d28b..241bb6d40 100644 --- a/markdown/extensions/fenced_code.py +++ b/markdown/extensions/fenced_code.py @@ -1,20 +1,25 @@ -""" -Fenced Code Extension for Python Markdown -========================================= +# Fenced Code Extension for Python Markdown +# ========================================= -This extension adds Fenced Code Blocks to Python-Markdown. +# This extension adds Fenced Code Blocks to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/fenced_code_blocks +# for documentation. -See -for documentation. +# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). -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) -All changes Copyright 2008-2014 The Python Markdown Project +""" +This extension adds Fenced Code Blocks to Python-Markdown. -License: [BSD](https://opensource.org/licenses/bsd-license.php) +See the [documentation](https://Python-Markdown.github.io/extensions/fenced_code_blocks) +for details. """ +from __future__ import annotations from textwrap import dedent from . import Extension @@ -31,6 +36,7 @@ def __init__(self, **kwargs): self.config = { 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] } + """ Default configuration options. """ super().__init__(**kwargs) def extendMarkdown(self, md): @@ -41,6 +47,8 @@ def extendMarkdown(self, md): class FencedBlockPreprocessor(Preprocessor): + """ Find and extract fenced code blocks. """ + FENCED_BLOCK_RE = re.compile( dedent(r''' (?P^(?:~{3,}|`{3,}))[ ]* # opening fence @@ -144,7 +152,7 @@ def run(self, lines): return text.split("\n") def handle_attrs(self, attrs): - """ Return tuple: (id, [list, of, classes], {configs}) """ + """ Return tuple: `(id, [list, of, classes], {configs})` """ id = '' classes = [] configs = {} diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py index 41074c050..cf1ab43a6 100644 --- a/markdown/extensions/footnotes.py +++ b/markdown/extensions/footnotes.py @@ -1,18 +1,24 @@ -""" -Footnotes Extension for Python-Markdown -======================================= +# Footnotes Extension for Python-Markdown +# ======================================= -Adds footnote handling to Python-Markdown. +# Adds footnote handling to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/footnotes +# for documentation. -See -for documentation. +# Copyright The Python Markdown Project -Copyright The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Adds footnote handling to Python-Markdown. +See the [documentation](https://Python-Markdown.github.io/extensions/footnotes) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import BlockProcessor from ..inlinepatterns import InlineProcessor @@ -36,30 +42,28 @@ 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."], - "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 " - "of the backlink. %d will be replaced by the " - "footnote number."], - "SEPARATOR": - [":", - "Footnote separator."] + '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." + ], + '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 of the backlink. ' + '%d will be replaced by the footnote number.' + ], + 'SEPARATOR': [ + ':', 'Footnote separator.' + ] } + """ Default configuration options. """ super().__init__(**kwargs) # In multiple invocations, emit links that don't get tangled. @@ -255,10 +259,11 @@ def run(self, parent, blocks): blocks.insert(0, block) return False - def detectTabbed(self, blocks): + def detectTabbed(self, blocks) -> list[str]: """ Find indented text and remove indent before further processing. - Returns: a list of blocks with indentation removed. + Returns: + A list of blocks with indentation removed. """ fn_blocks = [] while blocks: @@ -296,7 +301,7 @@ def detab(self, block): class FootnoteInlineProcessor(InlineProcessor): - """ `InlinePattern` for footnote markers in a document's body text. """ + """ `InlineProcessor` for footnote markers in a document's body text. """ def __init__(self, pattern, footnotes): super().__init__(pattern) diff --git a/markdown/extensions/legacy_attrs.py b/markdown/extensions/legacy_attrs.py index 6e05e7677..56ad2e89a 100644 --- a/markdown/extensions/legacy_attrs.py +++ b/markdown/extensions/legacy_attrs.py @@ -1,26 +1,23 @@ -""" -Python Markdown - -A Python implementation of John Gruber's Markdown. +# Python Markdown -Documentation: https://python-markdown.github.io/ -GitHub: https://github.com/Python-Markdown/markdown/ -PyPI: https://pypi.org/project/Markdown/ +# A Python implementation of John Gruber's 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). +# Documentation: https://python-markdown.github.io/ +# GitHub: https://github.com/Python-Markdown/markdown/ +# PyPI: https://pypi.org/project/Markdown/ -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) +# 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). -License: BSD (see LICENSE.md for details). +# Copyright 2007-2023 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) -Legacy Attributes Extension -=========================== +# License: BSD (see LICENSE.md for details). +""" An extension to Python Markdown which implements legacy attributes. Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes` @@ -31,6 +28,8 @@ years. This extension can be used to continue to render those documents correctly. """ +from __future__ import annotations + import re from markdown.treeprocessors import Treeprocessor, isString from markdown.extensions import Extension @@ -60,6 +59,7 @@ def attributeCallback(match): class LegacyAttrExtension(Extension): def extendMarkdown(self, md): + """ Add `LegacyAttrs` to Markdown instance. """ md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15) diff --git a/markdown/extensions/legacy_em.py b/markdown/extensions/legacy_em.py index bc147a69c..a6f67b7ef 100644 --- a/markdown/extensions/legacy_em.py +++ b/markdown/extensions/legacy_em.py @@ -1,14 +1,17 @@ -''' -Legacy Em Extension for Python-Markdown -======================================= +# Legacy Em Extension for Python-Markdown +# ======================================= -This extension provides legacy behavior for _connected_words_. +# This extension provides legacy behavior for _connected_words_. + +# Copyright 2015-2018 The Python Markdown Project -Copyright 2015-2018 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +This extension provides legacy behavior for _connected_words_. +""" -''' +from __future__ import annotations from . import Extension from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE diff --git a/markdown/extensions/md_in_html.py b/markdown/extensions/md_in_html.py index 92afecfee..982d60399 100644 --- a/markdown/extensions/md_in_html.py +++ b/markdown/extensions/md_in_html.py @@ -1,19 +1,26 @@ -""" -Python-Markdown Markdown in HTML Extension -=============================== +# 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. +# 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. -See -for documentation. +# Copyright The Python Markdown Project -Copyright The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s +parsing of Markdown syntax in raw HTML. +See the [documentation](https://Python-Markdown.github.io/extensions/raw_html) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import BlockProcessor from ..preprocessors import Preprocessor diff --git a/markdown/extensions/meta.py b/markdown/extensions/meta.py index 8a9386366..147d3b93c 100644 --- a/markdown/extensions/meta.py +++ b/markdown/extensions/meta.py @@ -1,20 +1,26 @@ -""" -Meta Data Extension for Python-Markdown -======================================= +# Meta Data Extension for Python-Markdown +# ======================================= -This extension adds Meta Data handling to markdown. +# This extension adds Meta Data handling to markdown. + +# See https://Python-Markdown.github.io/extensions/meta_data +# for documentation. -See -for documentation. +# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com). -Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com). +# All changes Copyright 2008-2014 The Python Markdown Project -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +This extension adds Meta Data handling to markdown. +See the [documentation](https://Python-Markdown.github.io/extensions/meta_data) +for details. """ +from __future__ import annotations + from . import Extension from ..preprocessors import Preprocessor import re diff --git a/markdown/extensions/nl2br.py b/markdown/extensions/nl2br.py index 23d56034a..177df1ee4 100644 --- a/markdown/extensions/nl2br.py +++ b/markdown/extensions/nl2br.py @@ -1,21 +1,28 @@ -""" -`NL2BR` Extension -=============== +# `NL2BR` Extension +# =============== -A Python-Markdown extension to treat newlines as hard breaks; like -GitHub-flavored Markdown does. +# 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. -See -for documentation. +# Original code Copyright 2011 [Brian Neal](https://deathofagremmie.com/) -Original code Copyright 2011 [Brian Neal](https://deathofagremmie.com/) +# All changes Copyright 2011-2014 The Python Markdown Project -All changes Copyright 2011-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +A Python-Markdown extension to treat newlines as hard breaks; like +GitHub-flavored Markdown does. +See the [documentation](https://Python-Markdown.github.io/extensions/nl2br) +for details. """ +from __future__ import annotations + from . import Extension from ..inlinepatterns import SubstituteTagInlineProcessor @@ -25,6 +32,7 @@ class Nl2BrExtension(Extension): def extendMarkdown(self, md): + """ Add a `SubstituteTagInlineProcessor` to Markdown. """ br_tag = SubstituteTagInlineProcessor(BR_RE, 'br') md.inlinePatterns.register(br_tag, 'nl', 5) diff --git a/markdown/extensions/sane_lists.py b/markdown/extensions/sane_lists.py index e27eb1803..305bd9923 100644 --- a/markdown/extensions/sane_lists.py +++ b/markdown/extensions/sane_lists.py @@ -1,29 +1,38 @@ -""" -Sane List Extension for Python-Markdown -======================================= +# Sane List Extension for Python-Markdown +# ======================================= -Modify the behavior of Lists in Python-Markdown to act in a sane manor. +# 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. -See -for documentation. +# Original code Copyright 2011 [Waylan Limberg](http://achinghead.com) -Original code Copyright 2011 [Waylan Limberg](http://achinghead.com) +# All changes Copyright 2011-2014 The Python Markdown Project -All changes Copyright 2011-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Modify the behavior of Lists in Python-Markdown to act in a sane manor. +See [documentation](https://Python-Markdown.github.io/extensions/sane_lists) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import OListProcessor, UListProcessor import re class SaneOListProcessor(OListProcessor): + """ Override `SIBLING_TAGS` to not include `ul` and set `LAZY_OL` to `False`. """ SIBLING_TAGS = ['ol'] + """ Exclude `ul` from list of siblings. """ LAZY_OL = False + """ Disable lazy list behavior. """ def __init__(self, parser): super().__init__(parser) @@ -32,8 +41,10 @@ def __init__(self, parser): class SaneUListProcessor(UListProcessor): + """ Override `SIBLING_TAGS` to not include `ol`. """ SIBLING_TAGS = ['ul'] + """ Exclude `ol` from list of siblings. """ def __init__(self, parser): super().__init__(parser) diff --git a/markdown/extensions/smarty.py b/markdown/extensions/smarty.py index f03169e00..ca6a017d3 100644 --- a/markdown/extensions/smarty.py +++ b/markdown/extensions/smarty.py @@ -1,84 +1,90 @@ -''' -Smarty extension for Python-Markdown -==================================== +# Smarty extension for Python-Markdown +# ==================================== -Adds conversion of ASCII dashes, quotes and ellipses to their HTML -entity equivalents. +# Adds conversion of ASCII dashes, quotes and ellipses to their HTML +# entity equivalents. + +# See https://Python-Markdown.github.io/extensions/smarty +# for documentation. -See -for documentation. +# Author: 2013, Dmitry Shachnev -Author: 2013, Dmitry Shachnev +# All changes Copyright 2013-2014 The Python Markdown Project -All changes Copyright 2013-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +# SmartyPants license: -SmartyPants license: +# Copyright (c) 2003 John Gruber +# All rights reserved. - Copyright (c) 2003 John Gruber - All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: - 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 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. - * 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. - * 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. - 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` license: +# `smartypants.py` is a derivative work of SmartyPants. +# Copyright (c) 2004, 2007 Chad Miller - `smartypants.py` is a derivative work of SmartyPants. - Copyright (c) 2004, 2007 Chad Miller +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: - 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 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. - * 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. - 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. +""" +Adds conversion of ASCII dashes, quotes and ellipses to their HTML +entity equivalents. -''' +See the [documentation](https://Python-Markdown.github.io/extensions/smarty) +for details. +""" +from __future__ import annotations from . import Extension from ..inlinepatterns import HtmlInlineProcessor, HTML_RE @@ -166,6 +172,7 @@ def handleMatch(self, m, data): class SmartyExtension(Extension): + """ Add Smarty to Markdown. """ def __init__(self, **kwargs): self.config = { 'smart_quotes': [True, 'Educate quotes'], @@ -174,6 +181,7 @@ def __init__(self, **kwargs): 'smart_ellipses': [True, 'Educate ellipses'], 'substitutions': [{}, 'Overwrite default substitutions'], } + """ Default configuration options. """ super().__init__(**kwargs) self.substitutions = dict(substitutions) self.substitutions.update(self.getConfig('substitutions', default={})) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index a72103f5c..a9e5f13d5 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -1,20 +1,26 @@ -""" -Tables Extension for Python-Markdown -==================================== +# Tables Extension for Python-Markdown +# ==================================== -Added parsing of tables to Python-Markdown. +# Added parsing of tables to Python-Markdown. + +# See https://Python-Markdown.github.io/extensions/tables +# for documentation. -See -for documentation. +# Original code Copyright 2009 [Waylan Limberg](http://achinghead.com) -Original code Copyright 2009 [Waylan Limberg](http://achinghead.com) +# All changes Copyright 2008-2014 The Python Markdown Project -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Added parsing of tables to Python-Markdown. +See the [documentation](https://Python-Markdown.github.io/extensions/tables) +for details. """ +from __future__ import annotations + from . import Extension from ..blockprocessors import BlockProcessor import xml.etree.ElementTree as etree @@ -221,6 +227,7 @@ def __init__(self, **kwargs): self.config = { 'use_align_attribute': [False, 'True to use align attribute instead of style.'], } + """ Default configuration options. """ super().__init__(**kwargs) diff --git a/markdown/extensions/toc.py b/markdown/extensions/toc.py index 6d080deb7..5490d0dd5 100644 --- a/markdown/extensions/toc.py +++ b/markdown/extensions/toc.py @@ -1,18 +1,24 @@ -""" -Table of Contents Extension for Python-Markdown -=============================================== +# Table of Contents Extension for Python-Markdown +# =============================================== + +# See https://Python-Markdown.github.io/extensions/toc +# for documentation. -See -for documentation. +# Original code Copyright 2008 [Jack Miller](https://codezen.org/) -Original code Copyright 2008 [Jack Miller](https://codezen.org/) +# All changes Copyright 2008-2014 The Python Markdown Project -All changes Copyright 2008-2014 The Python Markdown Project +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -License: [BSD](https://opensource.org/licenses/bsd-license.php) +""" +Add table of contents support to Python-Markdown. +See the [documentation](https://Python-Markdown.github.io/extensions/toc) +for details. """ +from __future__ import annotations + from . import Extension from ..treeprocessors import Treeprocessor from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString @@ -90,14 +96,16 @@ def unescape(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': []}]}] + + [{'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': []}] + + [{'level': 2}, {'level': 1}] + => + [{'level': 2, 'children': []}, {'level': 1, 'children': []}] """ ordered_list = [] @@ -152,6 +160,8 @@ def nest_toc_tokens(toc_list): class TocTreeprocessor(Treeprocessor): + """ Step through document and build TOC. """ + def __init__(self, md, config): super().__init__(md) @@ -178,7 +188,7 @@ def __init__(self, md, config): self.toc_bottom = int(config["toc_depth"]) def iterparent(self, node): - ''' Iterator wrapper to get allowed parent and child all at once. ''' + """ 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 endless loop of placing a new TOC @@ -189,7 +199,7 @@ def iterparent(self, node): yield from self.iterparent(child) def replace_marker(self, root, elem): - ''' Replace marker with elem. ''' + """ Replace marker with elem. """ for (p, c) in self.iterparent(root): text = ''.join(c.itertext()).strip() if not text: @@ -210,7 +220,7 @@ def replace_marker(self, root, elem): break def set_level(self, elem): - ''' Adjust header level according to base level. ''' + """ Adjust header level according to base level. """ level = int(elem.tag[-1]) + self.base_level if level > 6: level = 6 @@ -331,54 +341,58 @@ class TocExtension(Extension): 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
- " - "Defaults to an empty string"], - "title_class": ['toctitle', - 'CSS class used for the title. ' - 'Defaults to "toctitle"'], - "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"], - "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'"], - "permalink_leading": [False, - "True if permalinks should be placed at " - "the start of the header, rather than the " - "end - Defaults to False."], - "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 (

..) only.' - 'A string consisting of two digits separated by a hyphen' - 'in between ("2-5"), define the top (t) and the' - 'bottom (b) (..). Defaults to `6` (bottom).'], + 'marker': [ + '[TOC]', + 'Text to find and replace with Table of Contents. Set to an empty string to disable. ' + 'Default: `[TOC]`.' + ], + 'title': [ + '', 'Title to insert into TOC `
`. Default: an empty string.' + ], + 'title_class': [ + 'toctitle', 'CSS class used for the title. Default: `toctitle`.' + ], + 'toc_class': [ + 'toc', 'CSS class(es) used for the link. Default: `toclink`.' + ], + 'anchorlink': [ + False, 'True if header should be a self link. Default: `False`.' + ], + 'anchorlink_class': [ + 'toclink', 'CSS class(es) used for the link. Defaults: `toclink`.' + ], + 'permalink': [ + 0, 'True or link text if a Sphinx-style permalink should be added. Default: `False`.' + ], + 'permalink_class': [ + 'headerlink', 'CSS class(es) used for the link. Default: `headerlink`.' + ], + 'permalink_title': [ + 'Permanent link', 'Title attribute of the permalink. Default: `Permanent link`.' + ], + 'permalink_leading': [ + False, + 'True if permalinks should be placed at start of the header, rather than end. Default: False.' + ], + 'baselevel': ['1', 'Base level for headers. Default: `1`.'], + 'slugify': [ + slugify, 'Function to generate anchors based on header text. Default: `slugify`.' + ], + 'separator': ['-', 'Word separator. Default: `-`.'], + '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 (

..) only. A string consisting of two digits ' + 'separated by a hyphen in between (`2-5`) defines the top (t) and the bottom (b) (..). ' + 'Default: `6` (bottom).' + ], } + """ Default configuration options. """ super().__init__(**kwargs) def extendMarkdown(self, md): + """ Add TOC tree processor to Markdown. """ md.registerExtension(self) self.md = md self.reset() diff --git a/markdown/extensions/wikilinks.py b/markdown/extensions/wikilinks.py index d32d9122c..9d5acfa35 100644 --- a/markdown/extensions/wikilinks.py +++ b/markdown/extensions/wikilinks.py @@ -1,19 +1,25 @@ -''' -WikiLinks Extension for Python-Markdown -====================================== +# WikiLinks Extension for Python-Markdown +# ====================================== -Converts [[WikiLinks]] to relative links. +# Converts [[WikiLinks]] to relative links. -See -for documentation. +# See https://Python-Markdown.github.io/extensions/wikilinks +# for documentation. -Original code Copyright [Waylan Limberg](http://achinghead.com/). +# Original code Copyright [Waylan Limberg](http://achinghead.com/). -All changes Copyright The Python Markdown Project +# All changes Copyright The Python Markdown Project -License: [BSD](https://opensource.org/licenses/bsd-license.php) +# License: [BSD](https://opensource.org/licenses/bsd-license.php) -''' +""" +Converts `[[WikiLinks]]` to relative links. + +See the [documentation](https://Python-Markdown.github.io/extensions/wikilinks) +for details. +""" + +from __future__ import annotations from . import Extension from ..inlinepatterns import InlineProcessor @@ -28,6 +34,7 @@ def build_url(label, base, end): class WikiLinkExtension(Extension): + """ Add inline processor to Markdown. """ def __init__(self, **kwargs): self.config = { @@ -36,7 +43,7 @@ def __init__(self, **kwargs): 'html_class': ['wikilink', 'CSS hook. Leave blank for none.'], 'build_url': [build_url, 'Callable formats URL from label.'], } - + """ Default configuration options. """ super().__init__(**kwargs) def extendMarkdown(self, md): @@ -50,6 +57,8 @@ def extendMarkdown(self, md): class WikiLinksInlineProcessor(InlineProcessor): + """ Build link from `wikilink`. """ + def __init__(self, pattern, config): super().__init__(pattern) self.config = config diff --git a/markdown/htmlparser.py b/markdown/htmlparser.py index 78ac9588a..bf70b73de 100644 --- a/markdown/htmlparser.py +++ b/markdown/htmlparser.py @@ -1,23 +1,29 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). -License: BSD (see LICENSE.md for details). """ +This module imports a copy of [`html.parser.HTMLParser`][] and modifies it heavily through monkey-patches. +A copy is imported rather than the module being directly imported as this ensures that the user can import +and use the unmodified library for their own needs. +""" + +from __future__ import annotations import re import importlib.util @@ -65,8 +71,9 @@ 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. + The raw HTML is stored in the [`htmlStash`][markdown.util.HtmlStash] of the + [`Markdown`][markdown.Markdown] instance passed to `md` and the remaining text + is stored in `cleandoc` as a list of strings. """ def __init__(self, md, *args, **kwargs): @@ -105,7 +112,7 @@ def close(self): self._cache = [] @property - def line_offset(self): + def line_offset(self) -> int: """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) @@ -117,7 +124,7 @@ def line_offset(self): return self.rawdata.rfind('\n') return 0 - def at_line_start(self): + def at_line_start(self) -> bool: """ Returns True if current position is at start of line. @@ -130,7 +137,7 @@ def at_line_start(self): # 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): + def get_endtag_text(self, tag: str) -> str: """ Returns the text of the end tag. @@ -145,7 +152,7 @@ def get_endtag_text(self, tag): # Failed to extract from raw data. Assume well formed and lowercase. return ''.format(tag) - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: dict[str, str]): # 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) @@ -166,7 +173,7 @@ def handle_starttag(self, tag, attrs): # This is presumably a standalone tag in a code span (see #1036). self.clear_cdata_mode() - def handle_endtag(self, tag): + def handle_endtag(self, tag: str): text = self.get_endtag_text(tag) if self.inraw: @@ -193,7 +200,7 @@ def handle_endtag(self, tag): else: self.cleandoc.append(text) - def handle_data(self, data): + def handle_data(self, data: str): if self.intail and '\n' in data: self.intail = False if self.inraw: @@ -201,7 +208,7 @@ def handle_data(self, data): else: self.cleandoc.append(data) - def handle_empty_tag(self, data, is_block): + def handle_empty_tag(self, data: str, is_block: bool): """ Handle empty tags (``). """ if self.inraw or self.intail: # Append this to the existing raw block @@ -224,29 +231,29 @@ def handle_empty_tag(self, data, is_block): else: self.cleandoc.append(data) - def handle_startendtag(self, tag, attrs): + def handle_startendtag(self, tag: str, attrs: dict[str, str]): self.handle_empty_tag(self.get_starttag_text(), is_block=self.md.is_block_level(tag)) - def handle_charref(self, name): + def handle_charref(self, name: str): self.handle_empty_tag('&#{};'.format(name), is_block=False) - def handle_entityref(self, name): + def handle_entityref(self, name: str): self.handle_empty_tag('&{};'.format(name), is_block=False) - def handle_comment(self, data): + def handle_comment(self, data: str): self.handle_empty_tag(''.format(data), is_block=True) - def handle_decl(self, data): + def handle_decl(self, data: str): self.handle_empty_tag(''.format(data), is_block=True) - def handle_pi(self, data): + def handle_pi(self, data: str): self.handle_empty_tag(''.format(data), is_block=True) - def unknown_decl(self, data): + def unknown_decl(self, data: str): end = ']]>' if data.startswith('CDATA[') else ']>' self.handle_empty_tag(' int: 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 @@ -254,7 +261,7 @@ def parse_pi(self, i): self.handle_data(' int: 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 @@ -268,11 +275,11 @@ def parse_html_declaration(self, i): # can override `cdata_mode` in certain situations (in a code span). __starttag_text = None - def get_starttag_text(self): + def get_starttag_text(self) -> str: """Return full source of start tag: `<...>`.""" return self.__starttag_text - def parse_starttag(self, i): # pragma: no cover + def parse_starttag(self, i: int) -> int: # pragma: no cover self.__starttag_text = None endpos = self.check_for_whole_start_tag(i) if endpos < 0: diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py index ee7722e9c..7df7939f6 100644 --- a/markdown/inlinepatterns.py +++ b/markdown/inlinepatterns.py @@ -1,67 +1,48 @@ -""" -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) +# Python Markdown -License: BSD (see LICENSE.md for details). +# A Python implementation of John Gruber's Markdown. -INLINE PATTERNS -============================================================================= +# Documentation: https://python-markdown.github.io/ +# GitHub: https://github.com/Python-Markdown/markdown/ +# PyPI: https://pypi.org/project/Markdown/ -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: +# 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). - pattern.getCompiledRegExp() # returns a regular expression +# Copyright 2007-2023 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) - pattern.handleMatch(m) # takes a match object and returns - # an ElementTree element or just plain text +# License: BSD (see LICENSE.md for details). -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 `` 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. +""" +In version 3.0, a new, more flexible inline processor was added, [`markdown.inlinepatterns.InlineProcessor`][]. The +original inline patterns, which inherit from [`markdown.inlinepatterns.Pattern`][] or one of its children are still +supported, though users are encouraged to migrate. -* then we handle auto-links (must be done before inline html) +The new `InlineProcessor` provides two major enhancements to `Patterns`: -* 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. +1. Inline Processors no longer need to match the entire block, so regular expressions no longer need to start with + `r'^(.*?)'` and end with `r'(.*?)%'`. This runs faster. The returned [`Match`][re.Match] object will only contain + what is explicitly matched in the pattern, and extension pattern groups now start with `m.group(1)`. -* then inline images (must be done before links) +2. The `handleMatch` method now takes an additional input called `data`, which is the entire block under analysis, + not just what is matched with the specified pattern. The method now returns the element *and* the indexes relative + to `data` that the return element is replacing (usually `m.start(0)` and `m.end(0)`). If the boundaries are + returned as `None`, it is assumed that the match did not take place, and nothing will be altered in `data`. -* then bracketed links, first regular then reference-style + This allows handling of more complex constructs than regular expressions can handle, e.g., matching nested + brackets, and explicit control of the span "consumed" by the processor. -* finally we apply strong and emphasis """ +from __future__ import annotations + from . import util from collections import namedtuple +from typing import TYPE_CHECKING, Match, Any import re import xml.etree.ElementTree as etree try: # pragma: no cover @@ -69,9 +50,29 @@ except ImportError: # pragma: no cover import htmlentitydefs as entities +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown + + +def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry: + """ + Build the default set of inline patterns for Markdown. + + The order in which processors and/or patterns are applied is very important - e.g. if we first replace + `http://.../` links with `` tags and _then_ try to replace inline HTML, we would end up with a mess. So, we + apply the expressions in the following order: + + * backticks and escaped characters have to be handled before everything else so that we can preempt any markdown + patterns by escaping them; + + * then we handle the various types of links (auto-links must be handled 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 stash; -def build_inlinepatterns(md, **kwargs): - """ Build the default set of inline patterns for Markdown. """ + * finally we apply strong, emphasis, etc. + + """ inlinePatterns = util.Registry() inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190) inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md), 'escape', 180) @@ -98,81 +99,80 @@ def build_inlinepatterns(md, **kwargs): return inlinePatterns -""" -The actual regular expressions for patterns ------------------------------------------------------------------------------ -""" +# The actual regular expressions for patterns +# ----------------------------------------------------------------------------- NOIMG = r'(?)` or `[text](url "title")` LINK_RE = NOIMG + r'\[' +""" Match start of in-line link (`[text](url)` or `[text]()` or `[text](url "title")`). """ -# `![alttxt](http://x.com/)` or `![alttxt]()` IMAGE_LINK_RE = r'\!\[' +""" Match start of in-line image link (`![alttxt](url)` or `![alttxt]()`). """ -# `[Google][3]` REFERENCE_RE = LINK_RE +""" Match start of reference link (`[Label][3]`). """ -# `![alt text][2]` IMAGE_REFERENCE_RE = IMAGE_LINK_RE +""" Match start of image reference (`![alt text][2]`). """ -# stand-alone `*` or `_` NOT_STRONG_RE = r'((^|(?<=\s))(\*{1,3}|_{1,3})(?=\s|$))' +""" Match a stand-alone `*` or `_`. """ -# `` AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>' +""" Match an automatic link (``). """ -# `` AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>' +""" Match an automatic email link (``). """ -# `<...>` HTML_RE = r'(<(\/?[a-zA-Z][^<>@ ]*( [^<>]*)?|!--(?:(?!).)*--)>)' +""" Match an HTML tag (`<...>`). """ -# `&` (decimal) or `&` (hex) or `&` (named) ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)' +""" Match an HTML entity (`&` (decimal) or `&` (hex) or `&` (named)). """ -# two spaces at end of line LINE_BREAK_RE = r' \n' +""" Match two spaces at end of line. """ -def dequote(string): +def dequote(string: str) -> str: """Remove quotes from around a string.""" if ((string.startswith('"') and string.endswith('"')) or (string.startswith("'") and string.endswith("'"))): @@ -185,24 +185,45 @@ class EmStrongItem(namedtuple('EmStrongItem', ['pattern', 'builder', 'tags'])): """Emphasis/strong pattern item.""" -""" -The pattern classes ------------------------------------------------------------------------------ -""" +# The pattern classes +# ----------------------------------------------------------------------------- class Pattern: # pragma: no cover - """Base class that inline patterns subclass. """ + """ + Base class that inline patterns subclass. + + Inline patterns are handled by means of `Pattern` subclasses, one per regular expression. + Each pattern object uses a single regular expression and must support the following methods: + [`getCompiledRegExp`][markdown.inlinepatterns.Pattern.getCompiledRegExp] and + [`handleMatch`][markdown.inlinepatterns.Pattern.handleMatch]. + + All the regular expressions used by `Pattern` subclasses must capture the whole block. For this + reason, they all start with `^(.*)` and end with `(.*)!`. When passing a regular expression on + class initialization, the `^(.*)` and `(.*)!` are added automatically and the regular expression + is pre-compiled. + + It is strongly suggested that the newer style [`markdown.inlinepatterns.InlineProcessor`][] that + use a more efficient and flexible search approach be used instead. However, the older style + `Pattern` remains for backward compatibility with many existing third-party extensions. + + """ ANCESTOR_EXCLUDES = tuple() + """ + A collection of elements which are undesirable ancestors. The processor will be skipped if it + would cause the content to be a descendant of one of the listed tag names. + """ - def __init__(self, pattern, md=None): + def __init__(self, pattern: str, md: Markdown | None = None): """ Create an instant of an inline pattern. - Keyword arguments: + Arguments: + pattern: A regular expression that matches a pattern. + md: An optional pointer to the instance of `markdown.Markdown` and is available as + `self.md` on the class instance. - * pattern: A regular expression that matches a pattern """ self.pattern = pattern @@ -211,27 +232,28 @@ def __init__(self, pattern, md=None): self.md = md - def getCompiledRegExp(self): + def getCompiledRegExp(self) -> re.Pattern: """ Return a compiled regular expression. """ return self.compiled_re - def handleMatch(self, m): + def handleMatch(self, m: Match) -> etree.Element: """Return a ElementTree element from the given match. Subclasses should override this method. - Keyword arguments: + Arguments: + m: A match object containing a match of the pattern. - * m: A re match object containing a match of the pattern. + Returns: An ElementTree Element object. """ pass # pragma: no cover - def type(self): + def type(self) -> str: """ Return class name, to define pattern type """ return self.__class__.__name__ - def unescape(self, text): + def unescape(self, text: str) -> str: """ Return unescaped text given text with an inline placeholder. """ try: stash = self.md.treeprocessors['inline'].stashed_nodes @@ -252,19 +274,21 @@ def get_stash(m): class InlineProcessor(Pattern): """ - Base class that inline patterns subclass. + Base class that inline processors subclass. This is the newer style inline processor that uses a more efficient and flexible search approach. + """ - def __init__(self, pattern, md=None): + def __init__(self, pattern: str, md: Markdown | None = None): """ - Create an instant of an inline pattern. - - Keyword arguments: + Create an instant of an inline processor. - * pattern: A regular expression that matches a pattern + Arguments: + pattern: A regular expression that matches a pattern. + md: An optional pointer to the instance of `markdown.Markdown` and is available as + `self.md` on the class instance. """ self.pattern = pattern @@ -274,7 +298,7 @@ def __init__(self, pattern, md=None): self.safe_mode = False self.md = md - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element | str | None, int | None, int | None]: """Return a ElementTree element from the given match and the start and end index of the matched text. @@ -283,37 +307,45 @@ def handleMatch(self, m, data): Subclasses should override this method. - Keyword arguments: - - * m: A re match object containing a match of the pattern. - * data: The buffer current under analysis + Arguments: + m: A re match object containing a match of the pattern. + data: The buffer currently 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. + 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 a simple text of `group(2)` of a Pattern. """ + def handleMatch(self, m: Match) -> str: + """ Return string content of `group(2)` of a matching pattern. """ return m.group(2) class SimpleTextInlineProcessor(InlineProcessor): - """ Return a simple text of group(1) of a Pattern. """ - def handleMatch(self, m, data): + """ Return a simple text of `group(1)` of a Pattern. """ + def handleMatch(self, m: Match, data: str) -> tuple[str, int, int]: + """ Return string content of `group(1)` of a matching pattern. """ return m.group(1), m.start(0), m.end(0) class EscapeInlineProcessor(InlineProcessor): """ Return an escaped character. """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[str | None, int, int]: + """ + If the character matched by `group(1)` of a pattern is in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS] + then return the integer representing the character's Unicode code point (as returned by [`ord`][]) wrapped + in [`util.STX`][markdown.util.STX] and [`util.ETX`][markdown.util.ETX]. + + If the matched character is not in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS], then return `None`. + """ + 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) @@ -323,15 +355,28 @@ def handleMatch(self, m, data): class SimpleTagPattern(Pattern): # pragma: no cover """ - Return element of type `tag` with a text attribute of group(3) + Return element of type `tag` with a text attribute of `group(3)` of a Pattern. """ - def __init__(self, pattern, tag): + def __init__(self, pattern: str, tag: str): + """ + Create an instant of an simple tag pattern. + + Arguments: + pattern: A regular expression that matches a pattern. + tag: Tag of element. + + """ Pattern.__init__(self, pattern) self.tag = tag + """ The tag of the rendered element. """ - def handleMatch(self, m): + def handleMatch(self, m: Match) -> etree.Element: + """ + Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(3)` of a + matching pattern as the Element's text. + """ el = etree.Element(self.tag) el.text = m.group(3) return el @@ -339,15 +384,28 @@ def handleMatch(self, m): class SimpleTagInlineProcessor(InlineProcessor): """ - Return element of type `tag` with a text attribute of group(2) + Return element of type `tag` with a text attribute of `group(2)` of a Pattern. """ - def __init__(self, pattern, tag): + def __init__(self, pattern: str, tag: str): + """ + Create an instant of an simple tag processor. + + Arguments: + pattern: A regular expression that matches a pattern. + tag: Tag of element. + + """ InlineProcessor.__init__(self, pattern) self.tag = tag + """ The tag of the rendered element. """ - def handleMatch(self, m, data): # pragma: no cover + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: # pragma: no cover + """ + Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(2)` of a + matching pattern as the Element's text. + """ el = etree.Element(self.tag) el.text = m.group(2) return el, m.start(0), m.end(0) @@ -355,24 +413,35 @@ def handleMatch(self, m, data): # pragma: no cover class SubstituteTagPattern(SimpleTagPattern): # pragma: no cover """ Return an element of type `tag` with no children. """ - def handleMatch(self, m): + def handleMatch(self, m: Match) -> etree.Element: + """ Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """ return etree.Element(self.tag) class SubstituteTagInlineProcessor(SimpleTagInlineProcessor): """ Return an element of type `tag` with no children. """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: + """ Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """ return etree.Element(self.tag), m.start(0), m.end(0) class BacktickInlineProcessor(InlineProcessor): - """ Return a `` element containing the matching text. """ + """ Return a `` element containing the escaped matching text. """ def __init__(self, pattern): InlineProcessor.__init__(self, pattern) self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX) self.tag = 'code' + """ The tag of the rendered element. """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element | str, int, int]: + """ + If the match contains `group(3)` of a pattern, then return a `code` + [`Element`][xml.etree.ElementTree.Element] which contains HTML escaped text (with + [`code_escape`][markdown.util.code_escape]) as an [`AtomicString`][markdown.util.AtomicString]. + + If the match does not contain `group(3)` then return the text of `group(1)` backslash escaped. + + """ if m.group(3): el = etree.Element(self.tag) el.text = util.AtomicString(util.code_escape(m.group(3).strip())) @@ -387,7 +456,12 @@ class DoubleTagPattern(SimpleTagPattern): # pragma: no cover Useful for strong emphasis etc. """ - def handleMatch(self, m): + def handleMatch(self, m: Match) -> etree.Element: + """ + Return [`Element`][xml.etree.ElementTree.Element] in following format: + `group(3)group(4)` where `group(4)` is optional. + + """ tag1, tag2 = self.tag.split(",") el1 = etree.Element(tag1) el2 = etree.SubElement(el1, tag2) @@ -403,7 +477,12 @@ class DoubleTagInlineProcessor(SimpleTagInlineProcessor): Useful for strong emphasis etc. """ - def handleMatch(self, m, data): # pragma: no cover + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: # pragma: no cover + """ + Return [`Element`][xml.etree.ElementTree.Element] in following format: + `group(2)group(3)` where `group(3)` is optional. + + """ tag1, tag2 = self.tag.split(",") el1 = etree.Element(tag1) el2 = etree.SubElement(el1, tag2) @@ -415,7 +494,8 @@ def handleMatch(self, m, data): # pragma: no cover class HtmlInlineProcessor(InlineProcessor): """ Store raw inline html and return a placeholder. """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[str, int, int]: + """ Store the text of `group(1)` of a pattern and return a placeholder string. """ rawhtml = self.backslash_unescape(self.unescape(m.group(1))) place_holder = self.md.htmlStash.store(rawhtml) return place_holder, m.start(0), m.end(0) @@ -461,6 +541,7 @@ class AsteriskProcessor(InlineProcessor): EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') ] + """ The various strong and emphasis patterns handled by this processor. """ def build_single(self, m, tag, idx): """Return single tag.""" @@ -570,7 +651,7 @@ def build_element(self, m, builder, tags, index): else: return self.build_single(m, tags, index) - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: """Parse patterns.""" el = None @@ -597,6 +678,7 @@ class UnderscoreProcessor(AsteriskProcessor): EmStrongItem(re.compile(SMART_STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), EmStrongItem(re.compile(SMART_EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') ] + """ The various strong and emphasis patterns handled by this processor. """ class LinkInlineProcessor(InlineProcessor): @@ -604,7 +686,8 @@ class LinkInlineProcessor(InlineProcessor): RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE) RE_TITLE_CLEAN = re.compile(r'\s') - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element | None, int | None, int | None]: + """ Return an `a` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """ text, index, handled = self.getText(data, m.end(0)) if not handled: @@ -763,7 +846,8 @@ def getText(self, data, index): class ImageInlineProcessor(LinkInlineProcessor): """ Return a `img` element from the given match. """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element | None, int | None, int | None]: + """ Return an `img` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """ text, index, handled = self.getText(data, m.end(0)) if not handled: return None, None, None @@ -789,7 +873,11 @@ class ReferenceInlineProcessor(LinkInlineProcessor): RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE) - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element | None, int | None, int | None]: + """ + Return [`Element`][xml.etree.ElementTree.Element] returned by `makeTag` method or `(None, None, None)`. + + """ text, index, handled = self.getText(data, m.end(0)) if not handled: return None, None, None @@ -809,9 +897,9 @@ def handleMatch(self, m, data): def evalId(self, data, index, text): """ - Evaluate the id portion of [ref][id]. + Evaluate the id portion of `[ref][id]`. - If [ref][] use [ref]. + If `[ref][]` use `[ref]`. """ m = self.RE_LINK.match(data, pos=index) if not m: @@ -823,7 +911,8 @@ def evalId(self, data, index, text): id = text.lower() return id, end, True - def makeTag(self, href, title, text): + def makeTag(self, href: str, title: str, text: str) -> etree.Element: + """ Return an `a` [`Element`][xml.etree.ElementTree.Element]. """ el = etree.Element('a') el.set('href', href) @@ -835,16 +924,17 @@ def makeTag(self, href, title, text): class ShortReferenceInlineProcessor(ReferenceInlineProcessor): - """Short form of reference: [google]. """ + """Short form of reference: `[google]`. """ def evalId(self, data, index, text): - """Evaluate the id from of [ref] """ + """Evaluate the id 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): + def makeTag(self, href: str, title: str, text: str) -> etree.Element: + """ Return an `img` [`Element`][xml.etree.ElementTree.Element]. """ el = etree.Element("img") el.set("src", href) if title: @@ -856,14 +946,15 @@ def makeTag(self, href, title, text): class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor): """ Short form of image reference: `![ref]`. """ def evalId(self, data, index, text): - """Evaluate the id from of [ref] """ + """Evaluate the id of `[ref]`. """ return text.lower(), index, True class AutolinkInlineProcessor(InlineProcessor): """ Return a link Element given an auto-link (``). """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: + """ Return an `a` [`Element`][xml.etree.ElementTree.Element] of `group(1)`. """ el = etree.Element("a") el.set('href', self.unescape(m.group(1))) el.text = util.AtomicString(m.group(1)) @@ -874,7 +965,8 @@ class AutomailInlineProcessor(InlineProcessor): """ Return a `mailto` link Element given an auto-mail link (``). """ - def handleMatch(self, m, data): + def handleMatch(self, m: Match, data: str) -> tuple[etree.Element, int, int]: + """ Return an [`Element`][xml.etree.ElementTree.Element] containing a `mailto` link of `group(1)`. """ el = etree.Element('a') email = self.unescape(m.group(1)) if email.startswith("mailto:"): diff --git a/markdown/postprocessors.py b/markdown/postprocessors.py index 0e89d406f..4a18e18ee 100644 --- a/markdown/postprocessors.py +++ b/markdown/postprocessors.py @@ -1,38 +1,43 @@ -""" -Python Markdown +# Python Markdown -A Python implementation of John Gruber's 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/ +# 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). +# 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) +# Copyright 2007-2023 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). +# 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. +Post-processors run on the text of the entire document after is has been serialized into a string. +Postprocessors should be used to work with the text just before output. Usually, they are used add +back sections that were extracted in a preprocessor, fix up outgoing encodings, or wrap the whole +document. """ +from __future__ import annotations + from collections import OrderedDict +from typing import TYPE_CHECKING, Any from . import util import re +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown -def build_postprocessors(md, **kwargs): + +def build_postprocessors(md: Markdown, **kwargs: Any) -> util.Registry: """ Build the default postprocessors for Markdown. """ postprocessors = util.Registry() postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30) @@ -44,16 +49,16 @@ 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 + 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. + Postprocessors must extend `Postprocessor`. """ - def run(self, text): + def run(self, text: str) -> str: """ - Subclasses of Postprocessor should implement a `run` method, which + Subclasses of `Postprocessor` should implement a `run` method, which takes the html document as a single text string and returns a (possibly modified) string. @@ -66,7 +71,7 @@ class RawHtmlPostprocessor(Postprocessor): BLOCK_LEVEL_REGEX = re.compile(r'^\<\/?([^ >]+)') - def run(self, text): + def run(self, text: str): """ Iterate over html stash and restore html. """ replacements = OrderedDict() for i in range(self.md.htmlStash.html_counter): @@ -99,7 +104,8 @@ def substitute_match(m): else: return self.run(processed_text) - def isblocklevel(self, html): + def isblocklevel(self, html: str) -> bool: + """ Check is block of HTML is block-level. """ m = self.BLOCK_LEVEL_REGEX.match(html) if m: if m.group(1)[0] in ('!', '?', '@', '%'): @@ -108,7 +114,7 @@ def isblocklevel(self, html): return self.md.is_block_level(m.group(1)) return False - def stash_to_string(self, text): + def stash_to_string(self, text: str) -> str: """ Convert a stashed object to a string. """ return str(text) @@ -122,11 +128,11 @@ def run(self, text): @util.deprecated( - "This class will be removed in the future; " - "use 'treeprocessors.UnescapeTreeprocessor' instead." + "This class is deprecated and will be removed in the future; " + "use [`UnescapeTreeprocessor`][markdown.treeprocessors.UnescapeTreeprocessor] instead." ) class UnescapePostprocessor(Postprocessor): - """ Restore escaped chars """ + """ Restore escaped chars. """ RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX)) diff --git a/markdown/preprocessors.py b/markdown/preprocessors.py index e1023c59a..8787e56bd 100644 --- a/markdown/preprocessors.py +++ b/markdown/preprocessors.py @@ -1,37 +1,41 @@ -""" -Python Markdown - -A Python implementation of John Gruber's Markdown. +# Python Markdown -Documentation: https://python-markdown.github.io/ -GitHub: https://github.com/Python-Markdown/markdown/ -PyPI: https://pypi.org/project/Markdown/ +# A Python implementation of John Gruber's 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). +# Documentation: https://python-markdown.github.io/ +# GitHub: https://github.com/Python-Markdown/markdown/ +# PyPI: https://pypi.org/project/Markdown/ -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) +# 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). -License: BSD (see LICENSE.md for details). +# Copyright 2007-2023 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) -PRE-PROCESSORS -============================================================================= +# License: BSD (see LICENSE.md for details). -Preprocessors work on source text before we start doing anything too -complicated. """ +Preprocessors work on source text before it is broken down into its individual parts. +This is an excellent place to clean up bad characters or to extract portions for later +processing that the parser may otherwise choke on. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Any from . import util from .htmlparser import HTMLExtractor import re +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown -def build_preprocessors(md, **kwargs): - """ Build the default set of preprocessors used by Markdown. """ + +def build_preprocessors(md: Markdown, **kwargs: Any) -> util.Registry: + """ Build and return 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) @@ -42,16 +46,16 @@ 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 + 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. + Preprocessors must extend `Preprocessor`. """ - def run(self, lines): + def run(self, lines: list[str]) -> list[str]: """ - Each subclass of Preprocessor should override the `run` method, which + 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. @@ -62,7 +66,7 @@ def run(self, lines): class NormalizeWhitespace(Preprocessor): """ Normalize whitespace for consistent parsing. """ - def run(self, lines): + def run(self, lines: list[str]) -> list[str]: source = '\n'.join(lines) source = source.replace(util.STX, "").replace(util.ETX, "") source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n" @@ -72,9 +76,14 @@ def run(self, lines): class HtmlBlockPreprocessor(Preprocessor): - """Remove html blocks from the text and store them for later retrieval.""" + """ + Remove html blocks from the text and store them for later retrieval. + + The raw HTML is stored in the [`htmlStash`][markdown.util.HtmlStash] of the + [`Markdown`][markdown.Markdown] instance. + """ - def run(self, lines): + def run(self, lines: list[str]) -> list[str]: source = '\n'.join(lines) parser = HTMLExtractor(self.md) parser.feed(source) diff --git a/markdown/serializers.py b/markdown/serializers.py index b5dcd11d0..5a8818e28 100644 --- a/markdown/serializers.py +++ b/markdown/serializers.py @@ -34,8 +34,19 @@ # OF THIS SOFTWARE. # -------------------------------------------------------------------- +""" +Python-Markdown provides two serializers which render [`ElementTree.Element`][xml.etree.ElementTree.Element] +objects to a string of HTML. Both functions wrap the same underlying code with only a few minor +differences as outlined below: + +1. Empty (self-closing) tags are rendered as `` for HTML and as `` for XHTML. +2. Boolean attributes are rendered as `attrname` for HTML and as `attrname="attrname"` for XHTML. +""" + +from __future__ import annotations + from xml.etree.ElementTree import ProcessingInstruction -from xml.etree.ElementTree import Comment, ElementTree, QName, HTML_EMPTY +from xml.etree.ElementTree import Comment, ElementTree, Element, QName, HTML_EMPTY import re __all__ = ['to_html_string', 'to_xhtml_string'] @@ -171,9 +182,12 @@ def _write_html(root, format="html"): # -------------------------------------------------------------------- # public functions -def to_html_string(element): + +def to_html_string(element: Element) -> str: + """ Serialize element and its children to a string of HTML5. """ return _write_html(ElementTree(element).getroot(), format="html") -def to_xhtml_string(element): +def to_xhtml_string(element: Element) -> str: + """ Serialize element and its children to a string of XHTML. """ return _write_html(ElementTree(element).getroot(), format="xhtml") diff --git a/markdown/test_tools.py b/markdown/test_tools.py index c4db6b22c..895e44ec5 100644 --- a/markdown/test_tools.py +++ b/markdown/test_tools.py @@ -1,28 +1,31 @@ -""" -Python Markdown +# Python Markdown -A Python implementation of John Gruber's 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/ +# 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). +# 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) +# Copyright 2007-2023 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). -""" +# License: BSD (see LICENSE.md for details). + +""" A collection of tools for testing the Markdown code base and extensions. """ + +from __future__ import annotations import os import sys import unittest import textwrap +from typing import Any from . import markdown, Markdown, util try: @@ -35,9 +38,9 @@ class TestCase(unittest.TestCase): """ - A `unittest.TestCase` subclass with helpers for testing Markdown output. + A [`unittest.TestCase`][] subclass with helpers for testing Markdown output. - Define `default_kwargs` as a dict of keywords to pass to Markdown for each + 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 @@ -52,13 +55,14 @@ class TestCase(unittest.TestCase): In all other respects, behaves as `unittest.TestCase`. """ - default_kwargs = {} + default_kwargs: dict[str, Any] = {} + """ Default options to pass to Markdown for each test. """ 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 + `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 @@ -93,10 +97,12 @@ class recursionlimit: Example usage: - with recursionlimit(20): - # test code here + ``` python + with recursionlimit(20): + # test code here + ``` - See https://stackoverflow.com/a/50120316/866026 + See . """ def __init__(self, limit): @@ -116,12 +122,12 @@ def __exit__(self, type, value, tb): class Kwargs(dict): - """ A dict like class for holding keyword arguments. """ + """ A `dict` like class for holding keyword arguments. """ pass def _normalize_whitespace(text): - """ Normalize whitespace for a string of html using `tidylib`. """ + """ Normalize whitespace for a string of HTML using `tidylib`. """ output, errors = tidylib.tidy_fragment(text, options={ 'drop_empty_paras': 0, 'fix_backslash': 0, @@ -189,22 +195,20 @@ def test(self): class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta): """ - A `unittest.TestCase` subclass for running Markdown's legacy file-based tests. + 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`. - `output_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. + Attributes: + location (str): A path to the directory of test files. An absolute path is preferred. + exclude (list[str]): A list of tests to exclude. Each test name should comprise the filename + without an extension. + normalize (bool): A boolean value indicating if the HTML should be normalized. Default: `False`. + input_ext (str): A string containing the file extension of input files. Default: `.txt`. + output_ext (str): A string containing the file extension of expected output files. Default: `html`. + default_kwargs (Kwargs[str, Any]): 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 diff --git a/markdown/treeprocessors.py b/markdown/treeprocessors.py index c779d4d7b..fceb18814 100644 --- a/markdown/treeprocessors.py +++ b/markdown/treeprocessors.py @@ -1,31 +1,42 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). -License: BSD (see LICENSE.md for details). """ +Tree processors manipulate the tree created by block processors. They can even create an entirely +new `ElementTree` object. This is an excellent place for creating summaries, adding collected +references, or last minute adjustments. + +""" + +from __future__ import annotations import re import xml.etree.ElementTree as etree +from typing import TYPE_CHECKING, Sequence, Any from . import util from . import inlinepatterns +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown -def build_treeprocessors(md, **kwargs): + +def build_treeprocessors(md: Markdown, **kwargs: Any) -> util.Registry: """ Build the default `treeprocessors` for Markdown. """ treeprocessors = util.Registry() treeprocessors.register(InlineProcessor(md), 'inline', 20) @@ -34,8 +45,8 @@ def build_treeprocessors(md, **kwargs): return treeprocessors -def isString(s): - """ Check if it's string """ +def isString(s: Any) -> bool: + """ Return `True` if object is a string but not an [`AtomicString`][markdown.util.AtomicString]. """ if not isinstance(s, util.AtomicString): return isinstance(s, str) return False @@ -46,17 +57,16 @@ class Treeprocessor(util.Processor): `Treeprocessor`s 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. + `Element` and modifies it as necessary. `Treeprocessors` must extend `markdown.Treeprocessor`. """ - def run(self, root): + def run(self, root: etree.Element) -> etree.Element | None: """ 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 + takes a root `Element`. This method can return another `Element` + object, and the existing root `Element` will be replaced, or it can modify the current tree and return `None`. """ pass # pragma: no cover @@ -83,16 +93,16 @@ def __makePlaceholder(self, type): hash = util.INLINE_PLACEHOLDER % id return hash, id - def __findPlaceholder(self, data, index): + def __findPlaceholder(self, data: str, index: int) -> tuple[str | None, int]: """ - Extract id from data string, start from index - - Keyword arguments: + Extract id from data string, start from index. - * `data`: string - * `index`: index, from which we start search + Arguments: + data: String. + index: Index, from which we start search. - Returns: placeholder id and string index, after the found placeholder. + Returns: + Placeholder id and string index, after the found placeholder. """ m = self.__placeholder_re.search(data, index) @@ -102,22 +112,21 @@ def __findPlaceholder(self, data, index): return None, index + 1 def __stashNode(self, node, type): - """ Add node to stash """ + """ Add node to stash. """ placeholder, id = self.__makePlaceholder(type) self.stashed_nodes[id] = node return placeholder - def __handleInline(self, data, patternIndex=0): + def __handleInline(self, data: str, patternIndex: int = 0) -> str: """ - Process string with inline patterns and replace it - with placeholders - - Keyword arguments: + Process string with inline patterns and replace it with placeholders. - * `data`: A line of Markdown text - * `patternIndex`: The index of the `inlinePattern` to start with + Arguments: + data: A line of Markdown text. + patternIndex: The index of the `inlinePattern` to start with. - Returns: String with placeholders. + Returns: + String with placeholders. """ if not isinstance(data, util.AtomicString): @@ -131,18 +140,15 @@ def __handleInline(self, data, patternIndex=0): patternIndex += 1 return data - def __processElementText(self, node, subnode, isText=True): + def __processElementText(self, node: etree.Element, subnode: etree.Element, isText: bool = 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`: boolean variable, True - it's text, False - it's a tail - - Returns: None + Arguments: + node: Parent node. + subnode: Processing node. + isText: Boolean variable, True - it's text, False - it's a tail. """ if isText: @@ -163,16 +169,17 @@ def __processElementText(self, node, subnode, isText=True): for newChild in childResult: node.insert(pos, newChild[0]) - def __processPlaceholders(self, data, parent, isText=True): + def __processPlaceholders(self, data: str, parent: etree.Element, isText: bool = True) -> list[etree.ElementTree]: """ 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 + Arguments: + data: String with placeholders instead of `ElementTree` elements. + parent: Element, which contains processing inline data. + isText: Boolean variable, True - it's text, False - it's a tail. - Returns: list with `ElementTree` elements with applied inline patterns. + Returns: + List with `ElementTree` elements with applied inline patterns. """ def linkText(text): @@ -238,19 +245,19 @@ def linkText(text): return result - def __applyPattern(self, pattern, data, patternIndex, startIndex=0): + def __applyPattern(self, pattern: str, data: str, patternIndex: int, startIndex: int = 0) -> tuple[str, bool, int]: """ 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 + 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. + Returns: + String with placeholders instead of `ElementTree` elements. """ new_style = isinstance(pattern, inlinepatterns.InlineProcessor) @@ -322,22 +329,22 @@ def __build_ancestors(self, parent, parents): ancestors.reverse() parents.extend(ancestors) - def run(self, tree, ancestors=None): + def run(self, tree: etree.Element, ancestors: Sequence[str] | None = None) -> etree.Element: """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`: + Iterate over `Element`, find elements with inline tag, apply inline + patterns and append newly created Elements to tree. To avoid further + processing of string with inline patterns, instead of normal string, + use subclass [`AtomicString`][markdown.util.AtomicString]: - node.text = markdown.AtomicString("This will not be processed.") + node.text = markdown.util.AtomicString("This will not be processed.") Arguments: + tree: `Element` object, representing Markdown tree. + ancestors: List of parent tag names that precede the tree node (if needed). - * `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. + Returns: + An element tree object with applied inline patterns. """ self.stashed_nodes = {} @@ -411,8 +418,8 @@ def _prettifyETree(self, elem): if not elem.tail or not elem.tail.strip(): elem.tail = i - def run(self, root): - """ Add line breaks to `ElementTree` root object. """ + def run(self, root: etree.Element) -> None: + """ Add line breaks to `Element` object and its children. """ self._prettifyETree(root) # Do `
`'s separately as they are often in the middle of diff --git a/markdown/util.py b/markdown/util.py index 09b8cf619..1ee602973 100644 --- a/markdown/util.py +++ b/markdown/util.py @@ -1,30 +1,39 @@ -""" -Python Markdown +# Python Markdown + +# A Python implementation of John Gruber's 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/ -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). -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-2023 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) -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). -License: BSD (see LICENSE.md for details). +""" +This module contains various contacts, classes and functions which get referenced and used +throughout the code base. """ +from __future__ import annotations + import re import sys import warnings from collections import namedtuple from functools import wraps, lru_cache from itertools import count +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: # pragma: no cover + from markdown import Markdown """ @@ -33,7 +42,7 @@ """ -BLOCK_LEVEL_ELEMENTS = [ +BLOCK_LEVEL_ELEMENTS: list[str] = [ # 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', @@ -45,23 +54,35 @@ 'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script', 'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video' ] +""" +List of HTML tags which get treated as block-level elements. Same as the `block_level_elements` +attribute of the [`Markdown`][markdown.Markdown] class. Generally one should use the +attribute on the class. This remains for compatibility with older extensions. +""" # Placeholders -STX = '\u0002' # Use `STX` ("Start of text") for start-of-placeholder -ETX = '\u0003' # Use `ETX` ("End of text") for end-of-placeholder +STX = '\u0002' +""" "Start of Text" marker for placeholder templates. """ +ETX = '\u0003' +""" "End of Text" marker for placeholder templates. """ INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:" +""" Prefix for inline placeholder template. """ INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX +""" Placeholder template for stashed inline text. """ INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)') +""" Regular Expression which matches inline placeholders. """ AMP_SUBSTITUTE = STX+"amp"+ETX +""" Placeholder template for HTML entities. """ HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX +""" Placeholder template for raw HTML. """ HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)') +""" Regular expression which matches HTML placeholders. """ TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX +""" Placeholder template for tags. """ -""" -Constants you probably do not need to change ------------------------------------------------------------------------------ -""" +# Constants you probably do not need to change +# ----------------------------------------------------------------------------- RTL_BIDI_RANGES = ( ('\u0590', '\u07FF'), @@ -72,14 +93,13 @@ ) -""" -AUXILIARY GLOBAL FUNCTIONS -============================================================================= -""" +# AUXILIARY GLOBAL FUNCTIONS +# ============================================================================= @lru_cache(maxsize=None) def get_installed_extensions(): + """ Return all entry_points in the `markdown.extensions` group. """ if sys.version_info >= (3, 10): from importlib import metadata else: # ` bool | None: + """Parses a string representing a boolean 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): @@ -129,8 +152,8 @@ def parseBoolValue(value, fail_on_errors=True, preserve_none=False): raise ValueError('Cannot parse bool value: %r' % value) -def code_escape(text): - """Escape code.""" +def code_escape(text: str) -> str: + """HTML escape a string of code.""" if "&" in text: text = text.replace("&", "&") if "<" in text: @@ -156,10 +179,8 @@ def nearing_recursion_limit(): return sys.getrecursionlimit() - _get_stack_depth() < 100 -""" -MISC AUXILIARY CLASSES -============================================================================= -""" +# MISC AUXILIARY CLASSES +# ============================================================================= class AtomicString(str): @@ -168,7 +189,16 @@ class AtomicString(str): class Processor: - def __init__(self, md=None): + """ The base class for all processors. + + Attributes: + Processor.md: The `Markdown` instance passed in an initialization. + + Arguments: + md: The `Markdown` instance this processor is a part of. + + """ + def __init__(self, md: Markdown = None): self.md = md @@ -185,17 +215,17 @@ def __init__(self): self.tag_counter = 0 self.tag_data = [] # list of dictionaries in the order tags appear - def store(self, html): + def store(self, html: str) -> str: """ 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. - * `html`: an html segment - - Returns : a placeholder string + Returns: + A placeholder string. """ self.rawHtmlBlocks.append(html) @@ -204,13 +234,14 @@ def store(self, html): return placeholder def reset(self): + """ Clear the stash. """ 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): + def store_tag(self, tag, attrs, left_index, right_index) -> str: """Store tag data and return a placeholder.""" self.tag_data.append({'tag': tag, 'attrs': attrs, 'left_index': left_index, @@ -299,7 +330,7 @@ def __len__(self): def __repr__(self): return '<{}({})>'.format(self.__class__.__name__, list(self)) - def get_index_for_name(self, name): + def get_index_for_name(self, name) -> int: """ Return the index of the given name. """ @@ -310,15 +341,14 @@ def get_index_for_name(self, name): ) raise ValueError('No item named "{}" exists.'.format(name)) - def register(self, item, name, priority): + def register(self, item: Any, name: str, priority: int): """ 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. + Arguments: + 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 @@ -337,7 +367,7 @@ def deregister(self, name, strict=True): """ Remove an item from the registry. - Set `strict=False` to fail silently. + Set `strict=False` to fail silently. Otherwise a [`ValueError`][] is raised for an unknown `name`. """ try: index = self.get_index_for_name(name) diff --git a/mkdocs.yml b/mkdocs.yml index 9cb1511d6..1db47dc59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ site_author: "The Python-Markdown Project" copyright: "Copyright © 2010-2023" use_directory_urls: true +watch: [markdown] theme: name: nature @@ -14,6 +15,7 @@ theme: extra_css: - custom.css + - mkdocstrings.css nav: - Python-Markdown: index.md @@ -42,6 +44,7 @@ nav: - WikiLinks: extensions/wikilinks.md - Extension API: extensions/api.md - Test Tools: test_tools.md + - API Reference: reference/ - Contributing to Python-Markdown: contributing.md - Changelog: changelog.md - Authors: authors.md @@ -59,3 +62,51 @@ markdown_extensions: - mdx_gh_links: user: Python-Markdown repo: markdown + +plugins: +- gen-files: + scripts: + - scripts/gen_ref_nav.py +- literate-nav: + nav_file: SUMMARY.md +- section-index +- mkdocstrings: + custom_templates: docs/templates + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + options: + annotations_path: brief + docstring_options: + ignore_init_summary: true + docstring_style: google + docstring_section_style: list + extensions: + - scripts/griffe_extensions.py:DeprecatedExtension + - scripts/griffe_extensions.py:PriorityTableExtension: + paths: + - markdown.preprocessors.build_preprocessors + - markdown.blockprocessors.build_block_parser + - markdown.treeprocessors.build_treeprocessors + - markdown.inlinepatterns.build_inlinepatterns + - markdown.postprocessors.build_postprocessors + filters: ["!^_"] + group_by_category: false + heading_level: 1 + inherited_members: true + members_order: source + merge_init_into_class: true + separate_signature: false + show_root_heading: true + show_object_full_path: true + show_signature_annotations: true + show_source: false + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: false + summary: true + source: + repo: https://github.com/Python-Markdown/markdown + tag: !!python/name:markdown.__version__ + title: "View source code on GitHub." diff --git a/pyproject.toml b/pyproject.toml index c75a0edb2..8c9e9bcfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,8 +54,12 @@ testing = [ ] docs = [ 'mkdocs>=1.5', - 'mkdocs-nature>=0.5', - 'mdx_gh_links>=0.2' + 'mkdocs-nature>=0.6', + 'mdx_gh_links>=0.2', + "mkdocstrings[python]", + "mkdocs-gen-files", + "mkdocs-section-index", + "mkdocs-literate-nav", ] [project.urls] diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py new file mode 100755 index 000000000..2ecbd5767 --- /dev/null +++ b/scripts/gen_ref_nav.py @@ -0,0 +1,60 @@ +"""Generate the code reference pages and navigation.""" + +import textwrap +import yaml +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() + +per_module_options = { + "markdown": {"summary": {"attributes": True, "functions": True, "classes": True}} +} + +modules = [ + Path("markdown", "__init__.py"), + Path("markdown", "preprocessors.py"), + Path("markdown", "blockparser.py"), + Path("markdown", "blockprocessors.py"), + Path("markdown", "treeprocessors.py"), + Path("markdown", "inlinepatterns.py"), + Path("markdown", "postprocessors.py"), + Path("markdown", "serializers.py"), + Path("markdown", "util.py"), + Path("markdown", "htmlparser.py"), + Path("markdown", "test_tools.py"), + *sorted(Path("markdown", "extensions").rglob("*.py")), +] + +for path in modules: + module_path = path.with_suffix("") + doc_path = path.with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1].startswith("_"): + continue + + nav_parts = [f"{part}" for part in parts] + nav[nav_parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}") + if ident in per_module_options: + yaml_options = yaml.dump({"options": per_module_options[ident]}) + fd.write(f"\n{textwrap.indent(yaml_options, prefix=' ')}") + elif ident.startswith("markdown.extensions."): + yaml_options = yaml.dump({"options": {"inherited_members": False}}) + fd.write(f"\n{textwrap.indent(yaml_options, prefix=' ')}") + + mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/griffe_extensions.py b/scripts/griffe_extensions.py new file mode 100644 index 000000000..551687fde --- /dev/null +++ b/scripts/griffe_extensions.py @@ -0,0 +1,97 @@ +"""Griffe extensions.""" + +from __future__ import annotations + +import ast +from typing import TYPE_CHECKING +import textwrap + +from griffe import Docstring, Extension +from griffe.docstrings.dataclasses import DocstringSectionAdmonition, DocstringSectionText + +if TYPE_CHECKING: + from griffe import Class, Function, ObjectNode + + +def _deprecated(obj: Class | Function) -> str | None: + for decorator in obj.decorators: + if decorator.callable_path == "markdown.util.deprecated": + return ast.literal_eval(str(decorator.value.arguments[0])) + return None + + +class DeprecatedExtension(Extension): + """Griffe extension for `@markdown.util.deprecated` decorator support.""" + + def _insert_message(self, obj: Function | Class, message: str) -> None: + if not obj.docstring: + obj.docstring = Docstring("", parent=obj) + sections = obj.docstring.parsed + sections.insert(0, DocstringSectionAdmonition(kind="warning", text=message, title="Deprecated")) + + def on_class_instance(self, node: ast.AST | ObjectNode, cls: Class) -> None: # noqa: ARG002 + """Add section to docstrings of deprecated classes.""" + if message := _deprecated(cls): + self._insert_message(cls, message) + cls.labels.add("deprecated") + + def on_function_instance(self, node: ast.AST | ObjectNode, func: Function) -> None: # noqa: ARG002 + """Add section to docstrings of deprecated functions.""" + if message := _deprecated(func): + self._insert_message(func, message) + func.labels.add("deprecated") + + +class PriorityTableExtension(Extension): + """ Griffe extension to insert a table of processor priority in specified functions. """ + + def __init__(self, paths: list[str] | None = None) -> None: + super().__init__() + self.paths = paths + + def linked_obj(self, value: str, path: str) -> str: + """ Wrap object name in reference link. """ + return f'[`{value}`][{path}.{value}]' + + def on_function_instance(self, node: ast.AST | ObjectNode, func: Function) -> None: # noqa: ARG002 + """Add table to specified function docstrings.""" + if self.paths and func.path not in self.paths: + return # skip objects that were not selected + + # Table header + data = [ + 'Class Instance | Name | Priority', + '-------------- | ---- | :------:' + ] + + # Extract table body from source code of function. + for obj in node.body: + # Extract the arguments passed to `util.Registry.register`. + if isinstance(obj, ast.Expr) and isinstance(obj.value, ast.Call) and obj.value.func.attr == 'register': + _args = obj.value.args + cls = self.linked_obj(_args[0].func.id, func.path.rsplit('.', 1)[0]) + name = _args[1].value + priority = str(_args[2].value) + if func.name == ('build_inlinepatterns'): + # Include Pattern: first arg passed to class + if isinstance(_args[0].args[0], ast.Constant): + # Pattern is a string + value = f'`"{_args[0].args[0].value}"`' + else: + # Pattern is a variable + value = self.linked_obj(_args[0].args[0].id, func.path.rsplit('.', 1)[0]) + cls = f'{cls}({value})' + data.append(f'{cls} | `{name}` | `{priority}`') + + table = '\n'.join(data) + body = ( + f"Return a [`{func.returns.canonical_name}`][{func.returns.canonical_path}] instance which contains " + "the following collection of classes with their assigned names and priorities.\n\n" + f"{table}" + ) + + # Add to docstring. + if not func.docstring: + func.docstring = Docstring("", parent=func) + sections = func.docstring.parsed + sections.append(DocstringSectionText(body, title="Priority Table")) diff --git a/tests/__init__.py b/tests/__init__.py index 564ba3be4..19aa92b87 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_apis.py b/tests/test_apis.py index 82bc3b8c5..1305c5476 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 305ea8347..a9e789f19 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_legacy.py b/tests/test_legacy.py index 295588b0a..df4da4d71 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/__init__.py b/tests/test_syntax/__init__.py index 564ba3be4..19aa92b87 100644 --- a/tests/test_syntax/__init__.py +++ b/tests/test_syntax/__init__.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/__init__.py b/tests/test_syntax/blocks/__init__.py index 564ba3be4..19aa92b87 100644 --- a/tests/test_syntax/blocks/__init__.py +++ b/tests/test_syntax/blocks/__init__.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_blockquotes.py b/tests/test_syntax/blocks/test_blockquotes.py index 3422f9f76..9a114644a 100644 --- a/tests/test_syntax/blocks/test_blockquotes.py +++ b/tests/test_syntax/blocks/test_blockquotes.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_code_blocks.py b/tests/test_syntax/blocks/test_code_blocks.py index 54b686069..dbbb9c79a 100644 --- a/tests/test_syntax/blocks/test_code_blocks.py +++ b/tests/test_syntax/blocks/test_code_blocks.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_headers.py b/tests/test_syntax/blocks/test_headers.py index ca065a56c..da2ca14ff 100644 --- a/tests/test_syntax/blocks/test_headers.py +++ b/tests/test_syntax/blocks/test_headers.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_hr.py b/tests/test_syntax/blocks/test_hr.py index 85a51b32f..a8753977b 100644 --- a/tests/test_syntax/blocks/test_hr.py +++ b/tests/test_syntax/blocks/test_hr.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_html_blocks.py b/tests/test_syntax/blocks/test_html_blocks.py index 8623b3085..22b949834 100644 --- a/tests/test_syntax/blocks/test_html_blocks.py +++ b/tests/test_syntax/blocks/test_html_blocks.py @@ -13,7 +13,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/blocks/test_paragraphs.py b/tests/test_syntax/blocks/test_paragraphs.py index 9b7ba0357..f09731e72 100644 --- a/tests/test_syntax/blocks/test_paragraphs.py +++ b/tests/test_syntax/blocks/test_paragraphs.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/__init__.py b/tests/test_syntax/extensions/__init__.py index 564ba3be4..19aa92b87 100644 --- a/tests/test_syntax/extensions/__init__.py +++ b/tests/test_syntax/extensions/__init__.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_abbr.py b/tests/test_syntax/extensions/test_abbr.py index 64388c2d8..fbb25ffb2 100644 --- a/tests/test_syntax/extensions/test_abbr.py +++ b/tests/test_syntax/extensions/test_abbr.py @@ -13,7 +13,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_attr_list.py b/tests/test_syntax/extensions/test_attr_list.py index f6a4fe65b..ba8b23693 100644 --- a/tests/test_syntax/extensions/test_attr_list.py +++ b/tests/test_syntax/extensions/test_attr_list.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_footnotes.py b/tests/test_syntax/extensions/test_footnotes.py index 9fe786bad..6f504e39c 100644 --- a/tests/test_syntax/extensions/test_footnotes.py +++ b/tests/test_syntax/extensions/test_footnotes.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_legacy_attrs.py b/tests/test_syntax/extensions/test_legacy_attrs.py index b4ba5e715..44e8a6159 100644 --- a/tests/test_syntax/extensions/test_legacy_attrs.py +++ b/tests/test_syntax/extensions/test_legacy_attrs.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_legacy_em.py b/tests/test_syntax/extensions/test_legacy_em.py index c5daf1cf8..ad2862e5e 100644 --- a/tests/test_syntax/extensions/test_legacy_em.py +++ b/tests/test_syntax/extensions/test_legacy_em.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/extensions/test_md_in_html.py b/tests/test_syntax/extensions/test_md_in_html.py index 3de49b072..c439a03b2 100644 --- a/tests/test_syntax/extensions/test_md_in_html.py +++ b/tests/test_syntax/extensions/test_md_in_html.py @@ -13,7 +13,7 @@ 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 2007-2023 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) @@ -1208,7 +1208,7 @@ def test_md1_nested_footnote_ref(self): def load_tests(loader, tests, pattern): - ''' Ensure `TestHTMLBlocks` doesn't get run twice by excluding it here. ''' + """ Ensure `TestHTMLBlocks` doesn't get run twice by excluding it here. """ suite = TestSuite() for test_class in [TestDefaultwMdInHTML, TestMdInHTML, TestMarkdownInHTMLPostProcessor]: tests = loader.loadTestsFromTestCase(test_class) diff --git a/tests/test_syntax/extensions/test_tables.py b/tests/test_syntax/extensions/test_tables.py index 6a1a0d4fc..4e33d1aa3 100644 --- a/tests/test_syntax/extensions/test_tables.py +++ b/tests/test_syntax/extensions/test_tables.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/inline/__init__.py b/tests/test_syntax/inline/__init__.py index 564ba3be4..19aa92b87 100644 --- a/tests/test_syntax/inline/__init__.py +++ b/tests/test_syntax/inline/__init__.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/inline/test_entities.py b/tests/test_syntax/inline/test_entities.py index 34cc2e725..4262bce1b 100644 --- a/tests/test_syntax/inline/test_entities.py +++ b/tests/test_syntax/inline/test_entities.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/inline/test_images.py b/tests/test_syntax/inline/test_images.py index c9c7cb88d..379cc597a 100644 --- a/tests/test_syntax/inline/test_images.py +++ b/tests/test_syntax/inline/test_images.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/inline/test_links.py b/tests/test_syntax/inline/test_links.py index 04587560d..702756894 100644 --- a/tests/test_syntax/inline/test_links.py +++ b/tests/test_syntax/inline/test_links.py @@ -12,7 +12,7 @@ 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 2007-2023 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) diff --git a/tests/test_syntax/inline/test_raw_html.py b/tests/test_syntax/inline/test_raw_html.py index f75a616d6..f165f750a 100644 --- a/tests/test_syntax/inline/test_raw_html.py +++ b/tests/test_syntax/inline/test_raw_html.py @@ -12,7 +12,7 @@ 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 2007-2023 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)