You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
11 KiB
358 lines
11 KiB
""" |
|
Python Markdown |
|
|
|
A Python implementation of John Gruber's Markdown. |
|
|
|
Documentation: https://python-markdown.github.io/ |
|
GitHub: https://github.com/Python-Markdown/markdown/ |
|
PyPI: https://pypi.org/project/Markdown/ |
|
|
|
Started by Manfred Stienstra (http://www.dwerg.net/). |
|
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). |
|
Currently maintained by Waylan Limberg (https://github.com/waylan), |
|
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). |
|
|
|
Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later) |
|
Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) |
|
Copyright 2004 Manfred Stienstra (the original version) |
|
|
|
License: BSD (see LICENSE.md for details). |
|
""" |
|
|
|
import re |
|
import sys |
|
import warnings |
|
from collections import namedtuple |
|
from functools import wraps, lru_cache |
|
from itertools import count |
|
|
|
|
|
""" |
|
Constants you might want to modify |
|
----------------------------------------------------------------------------- |
|
""" |
|
|
|
|
|
BLOCK_LEVEL_ELEMENTS = [ |
|
# Elements which are invalid to wrap in a `<p>` tag. |
|
# See https://w3c.github.io/html/grouping-content.html#the-p-element |
|
'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', |
|
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', |
|
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', |
|
'p', 'pre', 'section', 'table', 'ul', |
|
# Other elements which Markdown should not be mucking up the contents of. |
|
'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'html', 'iframe', 'li', 'legend', |
|
'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script', |
|
'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video' |
|
] |
|
|
|
# Placeholders |
|
STX = '\u0002' # Use `STX` ("Start of text") for start-of-placeholder |
|
ETX = '\u0003' # Use `ETX` ("End of text") for end-of-placeholder |
|
INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:" |
|
INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX |
|
INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)') |
|
AMP_SUBSTITUTE = STX+"amp"+ETX |
|
HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX |
|
HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)') |
|
TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX |
|
|
|
|
|
""" |
|
Constants you probably do not need to change |
|
----------------------------------------------------------------------------- |
|
""" |
|
|
|
RTL_BIDI_RANGES = ( |
|
('\u0590', '\u07FF'), |
|
# Hebrew (0590-05FF), Arabic (0600-06FF), |
|
# Syriac (0700-074F), Arabic supplement (0750-077F), |
|
# Thaana (0780-07BF), Nko (07C0-07FF). |
|
('\u2D30', '\u2D7F') # Tifinagh |
|
) |
|
|
|
|
|
""" |
|
AUXILIARY GLOBAL FUNCTIONS |
|
============================================================================= |
|
""" |
|
|
|
|
|
@lru_cache(maxsize=None) |
|
def get_installed_extensions(): |
|
if sys.version_info >= (3, 10): |
|
from importlib import metadata |
|
else: # `<PY310` use backport |
|
import importlib_metadata as metadata |
|
# Only load extension entry_points once. |
|
return metadata.entry_points(group='markdown.extensions') |
|
|
|
|
|
def deprecated(message, stacklevel=2): |
|
""" |
|
Raise a `DeprecationWarning` when wrapped function/method is called. |
|
|
|
Usage: |
|
@deprecated("This method will be removed in version X; use Y instead.") |
|
def some_method()" |
|
pass |
|
""" |
|
def wrapper(func): |
|
@wraps(func) |
|
def deprecated_func(*args, **kwargs): |
|
warnings.warn( |
|
f"'{func.__name__}' is deprecated. {message}", |
|
category=DeprecationWarning, |
|
stacklevel=stacklevel |
|
) |
|
return func(*args, **kwargs) |
|
return deprecated_func |
|
return wrapper |
|
|
|
|
|
def parseBoolValue(value, fail_on_errors=True, preserve_none=False): |
|
"""Parses a string representing 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): |
|
if preserve_none and value is None: |
|
return value |
|
return bool(value) |
|
elif preserve_none and value.lower() == 'none': |
|
return None |
|
elif value.lower() in ('true', 'yes', 'y', 'on', '1'): |
|
return True |
|
elif value.lower() in ('false', 'no', 'n', 'off', '0', 'none'): |
|
return False |
|
elif fail_on_errors: |
|
raise ValueError('Cannot parse bool value: %r' % value) |
|
|
|
|
|
def code_escape(text): |
|
"""Escape code.""" |
|
if "&" in text: |
|
text = text.replace("&", "&") |
|
if "<" in text: |
|
text = text.replace("<", "<") |
|
if ">" in text: |
|
text = text.replace(">", ">") |
|
return text |
|
|
|
|
|
def _get_stack_depth(size=2): |
|
"""Get current stack depth, performantly. |
|
""" |
|
frame = sys._getframe(size) |
|
|
|
for size in count(size): |
|
frame = frame.f_back |
|
if not frame: |
|
return size |
|
|
|
|
|
def nearing_recursion_limit(): |
|
"""Return true if current stack depth is within 100 of maximum limit.""" |
|
return sys.getrecursionlimit() - _get_stack_depth() < 100 |
|
|
|
|
|
""" |
|
MISC AUXILIARY CLASSES |
|
============================================================================= |
|
""" |
|
|
|
|
|
class AtomicString(str): |
|
"""A string which should not be further processed.""" |
|
pass |
|
|
|
|
|
class Processor: |
|
def __init__(self, md=None): |
|
self.md = md |
|
|
|
|
|
class HtmlStash: |
|
""" |
|
This class is used for stashing HTML objects that we extract |
|
in the beginning and replace with place-holders. |
|
""" |
|
|
|
def __init__(self): |
|
""" Create an `HtmlStash`. """ |
|
self.html_counter = 0 # for counting inline html segments |
|
self.rawHtmlBlocks = [] |
|
self.tag_counter = 0 |
|
self.tag_data = [] # list of dictionaries in the order tags appear |
|
|
|
def store(self, html): |
|
""" |
|
Saves an HTML segment for later reinsertion. Returns a |
|
placeholder string that needs to be inserted into the |
|
document. |
|
|
|
Keyword arguments: |
|
|
|
* `html`: an html segment |
|
|
|
Returns : a placeholder string |
|
|
|
""" |
|
self.rawHtmlBlocks.append(html) |
|
placeholder = self.get_placeholder(self.html_counter) |
|
self.html_counter += 1 |
|
return placeholder |
|
|
|
def reset(self): |
|
self.html_counter = 0 |
|
self.rawHtmlBlocks = [] |
|
|
|
def get_placeholder(self, key): |
|
return HTML_PLACEHOLDER % key |
|
|
|
def store_tag(self, tag, attrs, left_index, right_index): |
|
"""Store tag data and return a placeholder.""" |
|
self.tag_data.append({'tag': tag, 'attrs': attrs, |
|
'left_index': left_index, |
|
'right_index': right_index}) |
|
placeholder = TAG_PLACEHOLDER % str(self.tag_counter) |
|
self.tag_counter += 1 # equal to the tag's index in `self.tag_data` |
|
return placeholder |
|
|
|
|
|
# Used internally by `Registry` for each item in its sorted list. |
|
# Provides an easier to read API when editing the code later. |
|
# For example, `item.name` is more clear than `item[0]`. |
|
_PriorityItem = namedtuple('PriorityItem', ['name', 'priority']) |
|
|
|
|
|
class Registry: |
|
""" |
|
A priority sorted registry. |
|
|
|
A `Registry` instance provides two public methods to alter the data of the |
|
registry: `register` and `deregister`. Use `register` to add items and |
|
`deregister` to remove items. See each method for specifics. |
|
|
|
When registering an item, a "name" and a "priority" must be provided. All |
|
items are automatically sorted by "priority" from highest to lowest. The |
|
"name" is used to remove ("deregister") and get items. |
|
|
|
A `Registry` instance it like a list (which maintains order) when reading |
|
data. You may iterate over the items, get an item and get a count (length) |
|
of all items. You may also check that the registry contains an item. |
|
|
|
When getting an item you may use either the index of the item or the |
|
string-based "name". For example: |
|
|
|
registry = Registry() |
|
registry.register(SomeItem(), 'itemname', 20) |
|
# Get the item by index |
|
item = registry[0] |
|
# Get the item by name |
|
item = registry['itemname'] |
|
|
|
When checking that the registry contains an item, you may use either the |
|
string-based "name", or a reference to the actual item. For example: |
|
|
|
someitem = SomeItem() |
|
registry.register(someitem, 'itemname', 20) |
|
# Contains the name |
|
assert 'itemname' in registry |
|
# Contains the item instance |
|
assert someitem in registry |
|
|
|
The method `get_index_for_name` is also available to obtain the index of |
|
an item using that item's assigned "name". |
|
""" |
|
|
|
def __init__(self): |
|
self._data = {} |
|
self._priority = [] |
|
self._is_sorted = False |
|
|
|
def __contains__(self, item): |
|
if isinstance(item, str): |
|
# Check if an item exists by this name. |
|
return item in self._data.keys() |
|
# Check if this instance exists. |
|
return item in self._data.values() |
|
|
|
def __iter__(self): |
|
self._sort() |
|
return iter([self._data[k] for k, p in self._priority]) |
|
|
|
def __getitem__(self, key): |
|
self._sort() |
|
if isinstance(key, slice): |
|
data = Registry() |
|
for k, p in self._priority[key]: |
|
data.register(self._data[k], k, p) |
|
return data |
|
if isinstance(key, int): |
|
return self._data[self._priority[key].name] |
|
return self._data[key] |
|
|
|
def __len__(self): |
|
return len(self._priority) |
|
|
|
def __repr__(self): |
|
return '<{}({})>'.format(self.__class__.__name__, list(self)) |
|
|
|
def get_index_for_name(self, name): |
|
""" |
|
Return the index of the given name. |
|
""" |
|
if name in self: |
|
self._sort() |
|
return self._priority.index( |
|
[x for x in self._priority if x.name == name][0] |
|
) |
|
raise ValueError('No item named "{}" exists.'.format(name)) |
|
|
|
def register(self, item, name, priority): |
|
""" |
|
Add an item to the registry with the given name and priority. |
|
|
|
Parameters: |
|
|
|
* `item`: The item being registered. |
|
* `name`: A string used to reference the item. |
|
* `priority`: An integer or float used to sort against all items. |
|
|
|
If an item is registered with a "name" which already exists, the |
|
existing item is replaced with the new item. Treat carefully as the |
|
old item is lost with no way to recover it. The new item will be |
|
sorted according to its priority and will **not** retain the position |
|
of the old item. |
|
""" |
|
if name in self: |
|
# Remove existing item of same name first |
|
self.deregister(name) |
|
self._is_sorted = False |
|
self._data[name] = item |
|
self._priority.append(_PriorityItem(name, priority)) |
|
|
|
def deregister(self, name, strict=True): |
|
""" |
|
Remove an item from the registry. |
|
|
|
Set `strict=False` to fail silently. |
|
""" |
|
try: |
|
index = self.get_index_for_name(name) |
|
del self._priority[index] |
|
del self._data[name] |
|
except ValueError: |
|
if strict: |
|
raise |
|
|
|
def _sort(self): |
|
""" |
|
Sort the registry by priority from highest to lowest. |
|
|
|
This method is called internally and should never be explicitly called. |
|
""" |
|
if not self._is_sorted: |
|
self._priority.sort(key=lambda item: item.priority, reverse=True) |
|
self._is_sorted = True
|
|
|