175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# :Copyright: © 2020 Günter Milde.
|
|||
|
|
# :License: Released under the terms of the `2-Clause BSD license`_, in short:
|
|||
|
|
#
|
|||
|
|
# Copying and distribution of this file, with or without modification,
|
|||
|
|
# are permitted in any medium without royalty provided the copyright
|
|||
|
|
# notice and this notice are preserved.
|
|||
|
|
# This file is offered as-is, without any warranty.
|
|||
|
|
#
|
|||
|
|
# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
|
|||
|
|
#
|
|||
|
|
# Revision: $Revision: 10136 $
|
|||
|
|
# Date: $Date: 2025-05-20 17:48:27 +0200 (Di, 20. Mai 2025) $
|
|||
|
|
"""
|
|||
|
|
A parser for CommonMark Markdown text using `recommonmark`__.
|
|||
|
|
|
|||
|
|
__ https://pypi.org/project/recommonmark/
|
|||
|
|
|
|||
|
|
.. important:: This module is deprecated.
|
|||
|
|
|
|||
|
|
* The "recommonmark" package is unmaintained and deprecated.
|
|||
|
|
This wrapper module will be removed in Docutils 1.0.
|
|||
|
|
|
|||
|
|
* The API is not settled and may change with any minor Docutils version.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
__docformat__ = 'reStructuredText'
|
|||
|
|
|
|||
|
|
from docutils import Component
|
|||
|
|
from docutils import nodes
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# If possible, import Sphinx's 'addnodes'
|
|||
|
|
from sphinx import addnodes
|
|||
|
|
except ImportError:
|
|||
|
|
# stub to prevent errors if Sphinx isn't installed
|
|||
|
|
import sys
|
|||
|
|
import types
|
|||
|
|
|
|||
|
|
class pending_xref(nodes.Inline, nodes.Element):
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
sys.modules['sphinx'] = sphinx = types.ModuleType('sphinx')
|
|||
|
|
sphinx.addnodes = addnodes = types.SimpleNamespace()
|
|||
|
|
addnodes.pending_xref = pending_xref
|
|||
|
|
try:
|
|||
|
|
import recommonmark
|
|||
|
|
from recommonmark.parser import CommonMarkParser
|
|||
|
|
except ImportError as err:
|
|||
|
|
raise ImportError(
|
|||
|
|
'Parsing "recommonmark" Markdown flavour requires the\n'
|
|||
|
|
' package https://pypi.org/project/recommonmark.'
|
|||
|
|
) from err
|
|||
|
|
else:
|
|||
|
|
if recommonmark.__version__ < '0.6.0':
|
|||
|
|
raise ImportError('The installed version of "recommonmark" is too old.'
|
|||
|
|
' Update with "pip install -U recommonmark".')
|
|||
|
|
|
|||
|
|
|
|||
|
|
# auxiliary function for `document.findall()`
|
|||
|
|
def is_literal(node):
|
|||
|
|
return isinstance(node, (nodes.literal, nodes.literal_block))
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Parser(CommonMarkParser):
|
|||
|
|
"""MarkDown parser based on recommonmark.
|
|||
|
|
|
|||
|
|
This parser is provisional:
|
|||
|
|
the API is not settled and may change with any minor Docutils version.
|
|||
|
|
"""
|
|||
|
|
supported = ('recommonmark', 'commonmark', 'markdown', 'md')
|
|||
|
|
"""Formats this parser supports."""
|
|||
|
|
|
|||
|
|
config_section = 'recommonmark parser'
|
|||
|
|
config_section_dependencies = ('parsers',)
|
|||
|
|
|
|||
|
|
def get_transforms(self):
|
|||
|
|
return Component.get_transforms(self) # + [AutoStructify]
|
|||
|
|
|
|||
|
|
def parse(self, inputstring, document):
|
|||
|
|
"""Wrapper of upstream method.
|
|||
|
|
|
|||
|
|
Ensure "line-length-limt". Report errors with `document.reporter`.
|
|||
|
|
"""
|
|||
|
|
# check for exorbitantly long lines
|
|||
|
|
for i, line in enumerate(inputstring.split('\n')):
|
|||
|
|
if len(line) > document.settings.line_length_limit:
|
|||
|
|
error = document.reporter.error(
|
|||
|
|
'Line %d exceeds the line-length-limit.'%(i+1))
|
|||
|
|
document.append(error)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# pass to upstream parser
|
|||
|
|
try:
|
|||
|
|
CommonMarkParser.parse(self, inputstring, document)
|
|||
|
|
except Exception as err:
|
|||
|
|
if document.settings.traceback:
|
|||
|
|
raise err
|
|||
|
|
error = document.reporter.error('Parsing with "recommonmark" '
|
|||
|
|
'returned the error:\n%s'%err)
|
|||
|
|
document.append(error)
|
|||
|
|
|
|||
|
|
# Post-Processing
|
|||
|
|
# ---------------
|
|||
|
|
|
|||
|
|
def finish_parse(self) -> None:
|
|||
|
|
"""Finalize parse details. Call at end of `self.parse()`."""
|
|||
|
|
|
|||
|
|
document = self.document
|
|||
|
|
|
|||
|
|
# merge adjoining Text nodes:
|
|||
|
|
for node in document.findall(nodes.TextElement):
|
|||
|
|
children = node.children
|
|||
|
|
i = 0
|
|||
|
|
while i+1 < len(children):
|
|||
|
|
if (isinstance(children[i], nodes.Text)
|
|||
|
|
and isinstance(children[i+1], nodes.Text)):
|
|||
|
|
children[i] = nodes.Text(children[i]+children.pop(i+1))
|
|||
|
|
children[i].parent = node
|
|||
|
|
else:
|
|||
|
|
i += 1
|
|||
|
|
|
|||
|
|
# remove empty Text nodes:
|
|||
|
|
for node in document.findall(nodes.Text):
|
|||
|
|
if not len(node):
|
|||
|
|
node.parent.remove(node)
|
|||
|
|
|
|||
|
|
# add "code" class argument to literal elements (inline and block)
|
|||
|
|
for node in document.findall(is_literal):
|
|||
|
|
if 'code' not in node['classes']:
|
|||
|
|
node['classes'].append('code')
|
|||
|
|
# move "language" argument to classes
|
|||
|
|
for node in document.findall(nodes.literal_block):
|
|||
|
|
if 'language' in node.attributes:
|
|||
|
|
node['classes'].append(node['language'])
|
|||
|
|
del node['language']
|
|||
|
|
|
|||
|
|
# replace raw nodes if raw is not allowed
|
|||
|
|
if not document.settings.raw_enabled:
|
|||
|
|
for node in document.findall(nodes.raw):
|
|||
|
|
message = document.reporter.warning('Raw content disabled.')
|
|||
|
|
if isinstance(node.parent, nodes.TextElement):
|
|||
|
|
msgid = document.set_id(message)
|
|||
|
|
problematic = nodes.problematic('', node.astext(),
|
|||
|
|
refid=msgid)
|
|||
|
|
node.parent.replace(node, problematic)
|
|||
|
|
prbid = document.set_id(problematic)
|
|||
|
|
message.add_backref(prbid)
|
|||
|
|
document.append(message)
|
|||
|
|
else:
|
|||
|
|
node.parent.replace(node, message)
|
|||
|
|
|
|||
|
|
# drop pending_xref (Sphinx cross reference extension)
|
|||
|
|
for node in document.findall(addnodes.pending_xref):
|
|||
|
|
reference = node.children[0]
|
|||
|
|
if 'name' not in reference:
|
|||
|
|
reference['name'] = nodes.fully_normalize_name(
|
|||
|
|
reference.astext())
|
|||
|
|
node.parent.replace(node, reference)
|
|||
|
|
# now we are ready to call the upstream function:
|
|||
|
|
super().finish_parse()
|
|||
|
|
|
|||
|
|
def visit_document(self, node) -> None:
|
|||
|
|
"""Dummy function to prevent spurious warnings.
|
|||
|
|
|
|||
|
|
cf. https://github.com/readthedocs/recommonmark/issues/177
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# Overwrite parent method with version that
|
|||
|
|
# doesn't pass deprecated `rawsource` argument to nodes.Text:
|
|||
|
|
def visit_text(self, mdnode) -> None:
|
|||
|
|
self.current_node.append(nodes.Text(mdnode.literal))
|