483 lines
16 KiB
Python
483 lines
16 KiB
Python
|
"""
|
||
|
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
|
||
|
from collections import namedtuple
|
||
|
from functools import wraps
|
||
|
import warnings
|
||
|
import xml.etree.ElementTree
|
||
|
from .pep562 import Pep562
|
||
|
from itertools import count
|
||
|
|
||
|
try:
|
||
|
from importlib import metadata
|
||
|
except ImportError:
|
||
|
# <PY38 use backport
|
||
|
import importlib_metadata as metadata
|
||
|
|
||
|
PY37 = (3, 7) <= sys.version_info
|
||
|
|
||
|
|
||
|
# TODO: Remove deprecated variables in a future release.
|
||
|
__deprecated__ = {
|
||
|
'etree': ('xml.etree.ElementTree', xml.etree.ElementTree),
|
||
|
'string_type': ('str', str),
|
||
|
'text_type': ('str', str),
|
||
|
'int2str': ('chr', chr),
|
||
|
'iterrange': ('range', range)
|
||
|
}
|
||
|
|
||
|
|
||
|
"""
|
||
|
Constants you might want to modify
|
||
|
-----------------------------------------------------------------------------
|
||
|
"""
|
||
|
|
||
|
|
||
|
BLOCK_LEVEL_ELEMENTS = [
|
||
|
# Elements which are invalid to wrap in a `<p>` tag.
|
||
|
# See https://w3c.github.io/html/grouping-content.html#the-p-element
|
||
|
'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl',
|
||
|
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
||
|
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol',
|
||
|
'p', 'pre', 'section', 'table', 'ul',
|
||
|
# Other elements which Markdown should not be mucking up the contents of.
|
||
|
'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'iframe', 'li', 'legend',
|
||
|
'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
|
||
|
'style', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
|
||
|
]
|
||
|
|
||
|
# Placeholders
|
||
|
STX = '\u0002' # Use STX ("Start of text") for start-of-placeholder
|
||
|
ETX = '\u0003' # Use ETX ("End of text") for end-of-placeholder
|
||
|
INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:"
|
||
|
INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX
|
||
|
INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)')
|
||
|
AMP_SUBSTITUTE = STX+"amp"+ETX
|
||
|
HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX
|
||
|
HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)')
|
||
|
TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX
|
||
|
|
||
|
|
||
|
"""
|
||
|
Constants you probably do not need to change
|
||
|
-----------------------------------------------------------------------------
|
||
|
"""
|
||
|
|
||
|
# Only load extension entry_points once.
|
||
|
INSTALLED_EXTENSIONS = metadata.entry_points().get('markdown.extensions', ())
|
||
|
RTL_BIDI_RANGES = (
|
||
|
('\u0590', '\u07FF'),
|
||
|
# Hebrew (0590-05FF), Arabic (0600-06FF),
|
||
|
# Syriac (0700-074F), Arabic supplement (0750-077F),
|
||
|
# Thaana (0780-07BF), Nko (07C0-07FF).
|
||
|
('\u2D30', '\u2D7F') # Tifinagh
|
||
|
)
|
||
|
|
||
|
|
||
|
"""
|
||
|
AUXILIARY GLOBAL FUNCTIONS
|
||
|
=============================================================================
|
||
|
"""
|
||
|
|
||
|
|
||
|
def deprecated(message, stacklevel=2):
|
||
|
"""
|
||
|
Raise a DeprecationWarning when wrapped function/method is called.
|
||
|
|
||
|
Borrowed from https://stackoverflow.com/a/48632082/866026
|
||
|
"""
|
||
|
def deprecated_decorator(func):
|
||
|
@wraps(func)
|
||
|
def deprecated_func(*args, **kwargs):
|
||
|
warnings.warn(
|
||
|
"'{}' is deprecated. {}".format(func.__name__, message),
|
||
|
category=DeprecationWarning,
|
||
|
stacklevel=stacklevel
|
||
|
)
|
||
|
return func(*args, **kwargs)
|
||
|
return deprecated_func
|
||
|
return deprecated_decorator
|
||
|
|
||
|
|
||
|
@deprecated("Use 'Markdown.is_block_level' instead.")
|
||
|
def isBlockLevel(tag):
|
||
|
"""Check if the tag is a block level HTML tag."""
|
||
|
if isinstance(tag, str):
|
||
|
return tag.lower().rstrip('/') in BLOCK_LEVEL_ELEMENTS
|
||
|
# Some ElementTree tags are not strings, so return False.
|
||
|
return False
|
||
|
|
||
|
|
||
|
def parseBoolValue(value, fail_on_errors=True, preserve_none=False):
|
||
|
"""Parses a string representing bool value. If parsing was successful,
|
||
|
returns True or False. If preserve_none=True, returns True, False,
|
||
|
or None. If parsing was not successful, raises ValueError, or, if
|
||
|
fail_on_errors=False, returns None."""
|
||
|
if not isinstance(value, str):
|
||
|
if preserve_none and value is None:
|
||
|
return value
|
||
|
return bool(value)
|
||
|
elif preserve_none and value.lower() == 'none':
|
||
|
return None
|
||
|
elif value.lower() in ('true', 'yes', 'y', 'on', '1'):
|
||
|
return True
|
||
|
elif value.lower() in ('false', 'no', 'n', 'off', '0', 'none'):
|
||
|
return False
|
||
|
elif fail_on_errors:
|
||
|
raise ValueError('Cannot parse bool value: %r' % value)
|
||
|
|
||
|
|
||
|
def code_escape(text):
|
||
|
"""Escape code."""
|
||
|
if "&" in text:
|
||
|
text = text.replace("&", "&")
|
||
|
if "<" in text:
|
||
|
text = text.replace("<", "<")
|
||
|
if ">" in text:
|
||
|
text = text.replace(">", ">")
|
||
|
return text
|
||
|
|
||
|
|
||
|
def _get_stack_depth(size=2):
|
||
|
"""Get stack size for caller's frame.
|
||
|
See https://stackoverflow.com/a/47956089/866026
|
||
|
"""
|
||
|
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 withing 100 of maximum limit."""
|
||
|
return sys.getrecursionlimit() - _get_stack_depth() < 100
|
||
|
|
||
|
|
||
|
"""
|
||
|
MISC AUXILIARY CLASSES
|
||
|
=============================================================================
|
||
|
"""
|
||
|
|
||
|
|
||
|
class AtomicString(str):
|
||
|
"""A string which should not be further processed."""
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Processor:
|
||
|
def __init__(self, md=None):
|
||
|
self.md = md
|
||
|
|
||
|
@property
|
||
|
@deprecated("Use 'md' instead.")
|
||
|
def markdown(self):
|
||
|
# TODO: remove this later
|
||
|
return self.md
|
||
|
|
||
|
|
||
|
class HtmlStash:
|
||
|
"""
|
||
|
This class is used for stashing HTML objects that we extract
|
||
|
in the beginning and replace with place-holders.
|
||
|
"""
|
||
|
|
||
|
def __init__(self):
|
||
|
""" Create a HtmlStash. """
|
||
|
self.html_counter = 0 # for counting inline html segments
|
||
|
self.rawHtmlBlocks = []
|
||
|
self.tag_counter = 0
|
||
|
self.tag_data = [] # list of dictionaries in the order tags appear
|
||
|
|
||
|
def store(self, html):
|
||
|
"""
|
||
|
Saves an HTML segment for later reinsertion. Returns a
|
||
|
placeholder string that needs to be inserted into the
|
||
|
document.
|
||
|
|
||
|
Keyword arguments:
|
||
|
|
||
|
* html: an html segment
|
||
|
|
||
|
Returns : a placeholder string
|
||
|
|
||
|
"""
|
||
|
self.rawHtmlBlocks.append(html)
|
||
|
placeholder = self.get_placeholder(self.html_counter)
|
||
|
self.html_counter += 1
|
||
|
return placeholder
|
||
|
|
||
|
def reset(self):
|
||
|
self.html_counter = 0
|
||
|
self.rawHtmlBlocks = []
|
||
|
|
||
|
def get_placeholder(self, key):
|
||
|
return HTML_PLACEHOLDER % key
|
||
|
|
||
|
def store_tag(self, tag, attrs, left_index, right_index):
|
||
|
"""Store tag data and return a placeholder."""
|
||
|
self.tag_data.append({'tag': tag, 'attrs': attrs,
|
||
|
'left_index': left_index,
|
||
|
'right_index': right_index})
|
||
|
placeholder = TAG_PLACEHOLDER % str(self.tag_counter)
|
||
|
self.tag_counter += 1 # equal to the tag's index in self.tag_data
|
||
|
return placeholder
|
||
|
|
||
|
|
||
|
# Used internally by `Registry` for each item in its sorted list.
|
||
|
# Provides an easier to read API when editing the code later.
|
||
|
# For example, `item.name` is more clear than `item[0]`.
|
||
|
_PriorityItem = namedtuple('PriorityItem', ['name', 'priority'])
|
||
|
|
||
|
|
||
|
class Registry:
|
||
|
"""
|
||
|
A priority sorted registry.
|
||
|
|
||
|
A `Registry` instance provides two public methods to alter the data of the
|
||
|
registry: `register` and `deregister`. Use `register` to add items and
|
||
|
`deregister` to remove items. See each method for specifics.
|
||
|
|
||
|
When registering an item, a "name" and a "priority" must be provided. All
|
||
|
items are automatically sorted by "priority" from highest to lowest. The
|
||
|
"name" is used to remove ("deregister") and get items.
|
||
|
|
||
|
A `Registry` instance it like a list (which maintains order) when reading
|
||
|
data. You may iterate over the items, get an item and get a count (length)
|
||
|
of all items. You may also check that the registry contains an item.
|
||
|
|
||
|
When getting an item you may use either the index of the item or the
|
||
|
string-based "name". For example:
|
||
|
|
||
|
registry = Registry()
|
||
|
registry.register(SomeItem(), 'itemname', 20)
|
||
|
# Get the item by index
|
||
|
item = registry[0]
|
||
|
# Get the item by name
|
||
|
item = registry['itemname']
|
||
|
|
||
|
When checking that the registry contains an item, you may use either the
|
||
|
string-based "name", or a reference to the actual item. For example:
|
||
|
|
||
|
someitem = SomeItem()
|
||
|
registry.register(someitem, 'itemname', 20)
|
||
|
# Contains the name
|
||
|
assert 'itemname' in registry
|
||
|
# Contains the item instance
|
||
|
assert someitem in registry
|
||
|
|
||
|
The method `get_index_for_name` is also available to obtain the index of
|
||
|
an item using that item's assigned "name".
|
||
|
"""
|
||
|
|
||
|
def __init__(self):
|
||
|
self._data = {}
|
||
|
self._priority = []
|
||
|
self._is_sorted = False
|
||
|
|
||
|
def __contains__(self, item):
|
||
|
if isinstance(item, str):
|
||
|
# Check if an item exists by this name.
|
||
|
return item in self._data.keys()
|
||
|
# Check if this instance exists.
|
||
|
return item in self._data.values()
|
||
|
|
||
|
def __iter__(self):
|
||
|
self._sort()
|
||
|
return iter([self._data[k] for k, p in self._priority])
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
self._sort()
|
||
|
if isinstance(key, slice):
|
||
|
data = Registry()
|
||
|
for k, p in self._priority[key]:
|
||
|
data.register(self._data[k], k, p)
|
||
|
return data
|
||
|
if isinstance(key, int):
|
||
|
return self._data[self._priority[key].name]
|
||
|
return self._data[key]
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self._priority)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<{}({})>'.format(self.__class__.__name__, list(self))
|
||
|
|
||
|
def get_index_for_name(self, name):
|
||
|
"""
|
||
|
Return the index of the given name.
|
||
|
"""
|
||
|
if name in self:
|
||
|
self._sort()
|
||
|
return self._priority.index(
|
||
|
[x for x in self._priority if x.name == name][0]
|
||
|
)
|
||
|
raise ValueError('No item named "{}" exists.'.format(name))
|
||
|
|
||
|
def register(self, item, name, priority):
|
||
|
"""
|
||
|
Add an item to the registry with the given name and priority.
|
||
|
|
||
|
Parameters:
|
||
|
|
||
|
* `item`: The item being registered.
|
||
|
* `name`: A string used to reference the item.
|
||
|
* `priority`: An integer or float used to sort against all items.
|
||
|
|
||
|
If an item is registered with a "name" which already exists, the
|
||
|
existing item is replaced with the new item. Tread carefully as the
|
||
|
old item is lost with no way to recover it. The new item will be
|
||
|
sorted according to its priority and will **not** retain the position
|
||
|
of the old item.
|
||
|
"""
|
||
|
if name in self:
|
||
|
# Remove existing item of same name first
|
||
|
self.deregister(name)
|
||
|
self._is_sorted = False
|
||
|
self._data[name] = item
|
||
|
self._priority.append(_PriorityItem(name, priority))
|
||
|
|
||
|
def deregister(self, name, strict=True):
|
||
|
"""
|
||
|
Remove an item from the registry.
|
||
|
|
||
|
Set `strict=False` to fail silently.
|
||
|
"""
|
||
|
try:
|
||
|
index = self.get_index_for_name(name)
|
||
|
del self._priority[index]
|
||
|
del self._data[name]
|
||
|
except ValueError:
|
||
|
if strict:
|
||
|
raise
|
||
|
|
||
|
def _sort(self):
|
||
|
"""
|
||
|
Sort the registry by priority from highest to lowest.
|
||
|
|
||
|
This method is called internally and should never be explicitly called.
|
||
|
"""
|
||
|
if not self._is_sorted:
|
||
|
self._priority.sort(key=lambda item: item.priority, reverse=True)
|
||
|
self._is_sorted = True
|
||
|
|
||
|
# Deprecated Methods which provide a smooth transition from OrderedDict
|
||
|
|
||
|
def __setitem__(self, key, value):
|
||
|
""" Register item with priorty 5 less than lowest existing priority. """
|
||
|
if isinstance(key, str):
|
||
|
warnings.warn(
|
||
|
'Using setitem to register a processor or pattern is deprecated. '
|
||
|
'Use the `register` method instead.',
|
||
|
DeprecationWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
if key in self:
|
||
|
# Key already exists, replace without altering priority
|
||
|
self._data[key] = value
|
||
|
return
|
||
|
if len(self) == 0:
|
||
|
# This is the first item. Set priority to 50.
|
||
|
priority = 50
|
||
|
else:
|
||
|
self._sort()
|
||
|
priority = self._priority[-1].priority - 5
|
||
|
self.register(value, key, priority)
|
||
|
else:
|
||
|
raise TypeError
|
||
|
|
||
|
def __delitem__(self, key):
|
||
|
""" Deregister an item by name. """
|
||
|
if key in self:
|
||
|
self.deregister(key)
|
||
|
warnings.warn(
|
||
|
'Using del to remove a processor or pattern is deprecated. '
|
||
|
'Use the `deregister` method instead.',
|
||
|
DeprecationWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
else:
|
||
|
raise KeyError('Cannot delete key {}, not registered.'.format(key))
|
||
|
|
||
|
def add(self, key, value, location):
|
||
|
""" Register a key by location. """
|
||
|
if len(self) == 0:
|
||
|
# This is the first item. Set priority to 50.
|
||
|
priority = 50
|
||
|
elif location == '_begin':
|
||
|
self._sort()
|
||
|
# Set priority 5 greater than highest existing priority
|
||
|
priority = self._priority[0].priority + 5
|
||
|
elif location == '_end':
|
||
|
self._sort()
|
||
|
# Set priority 5 less than lowest existing priority
|
||
|
priority = self._priority[-1].priority - 5
|
||
|
elif location.startswith('<') or location.startswith('>'):
|
||
|
# Set priority halfway between existing priorities.
|
||
|
i = self.get_index_for_name(location[1:])
|
||
|
if location.startswith('<'):
|
||
|
after = self._priority[i].priority
|
||
|
if i > 0:
|
||
|
before = self._priority[i-1].priority
|
||
|
else:
|
||
|
# Location is first item`
|
||
|
before = after + 10
|
||
|
else:
|
||
|
# location.startswith('>')
|
||
|
before = self._priority[i].priority
|
||
|
if i < len(self) - 1:
|
||
|
after = self._priority[i+1].priority
|
||
|
else:
|
||
|
# location is last item
|
||
|
after = before - 10
|
||
|
priority = before - ((before - after) / 2)
|
||
|
else:
|
||
|
raise ValueError('Not a valid location: "%s". Location key '
|
||
|
'must start with a ">" or "<".' % location)
|
||
|
self.register(value, key, priority)
|
||
|
warnings.warn(
|
||
|
'Using the add method to register a processor or pattern is deprecated. '
|
||
|
'Use the `register` method instead.',
|
||
|
DeprecationWarning,
|
||
|
stacklevel=2,
|
||
|
)
|
||
|
|
||
|
|
||
|
def __getattr__(name):
|
||
|
"""Get attribute."""
|
||
|
|
||
|
deprecated = __deprecated__.get(name)
|
||
|
if deprecated:
|
||
|
warnings.warn(
|
||
|
"'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
|
||
|
category=DeprecationWarning,
|
||
|
stacklevel=(3 if PY37 else 4)
|
||
|
)
|
||
|
return deprecated[1]
|
||
|
raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
|
||
|
|
||
|
|
||
|
if not PY37:
|
||
|
Pep562(__name__)
|