import time
from os import path
from docutils import frontend, nodes
from docutils.core import publish_doctree
from markdown_it.token import Token
from markdown_it.utils import AttrDict
from sphinx.application import Sphinx
from sphinx.io import SphinxStandaloneReader
from sphinx.parsers import Parser
from sphinx.util import logging
from sphinx.util.docutils import sphinx_domains
from myst_parser.main import MdParserConfig, default_parser
SPHINX_LOGGER = logging.getLogger(__name__)
[docs]class MystParser(Parser):
"""Docutils parser for Markedly Structured Text (MyST)."""
supported = ("md", "markdown", "myst")
translate_section_name = None
default_config: dict = {}
# these specs are copied verbatim from the docutils RST parser
settings_spec = (
"MyST Parser Options",
None,
(
(
'Recognize and link to standalone PEP references (like "PEP 258").',
["--pep-references"],
{"action": "store_true", "validator": frontend.validate_boolean},
),
(
"Base URL for PEP references "
'(default "http://www.python.org/dev/peps/").',
["--pep-base-url"],
{
"metavar": "<URL>",
"default": "http://www.python.org/dev/peps/",
"validator": frontend.validate_url_trailing_slash,
},
),
(
'Template for PEP file part of URL. (default "pep-%04d")',
["--pep-file-url-template"],
{"metavar": "<URL>", "default": "pep-%04d"},
),
(
'Recognize and link to standalone RFC references (like "RFC 822").',
["--rfc-references"],
{"action": "store_true", "validator": frontend.validate_boolean},
),
(
'Base URL for RFC references (default "http://tools.ietf.org/html/").',
["--rfc-base-url"],
{
"metavar": "<URL>",
"default": "http://tools.ietf.org/html/",
"validator": frontend.validate_url_trailing_slash,
},
),
(
"Set number of spaces for tab expansion (default 8).",
["--tab-width"],
{
"metavar": "<width>",
"type": "int",
"default": 8,
"validator": frontend.validate_nonnegative_int,
},
),
(
"Remove spaces before footnote references.",
["--trim-footnote-reference-space"],
{"action": "store_true", "validator": frontend.validate_boolean},
),
(
"Leave spaces before footnote references.",
["--leave-footnote-reference-space"],
{"action": "store_false", "dest": "trim_footnote_reference_space"},
),
(
"Disable directives that insert the contents of external file "
'("include" & "raw"); replaced with a "warning" system message.',
["--no-file-insertion"],
{
"action": "store_false",
"default": 1,
"dest": "file_insertion_enabled",
"validator": frontend.validate_boolean,
},
),
(
"Enable directives that insert the contents of external file "
'("include" & "raw"). Enabled by default.',
["--file-insertion-enabled"],
{"action": "store_true"},
),
(
'Disable the "raw" directives; replaced with a "warning" '
"system message.",
["--no-raw"],
{
"action": "store_false",
"default": 1,
"dest": "raw_enabled",
"validator": frontend.validate_boolean,
},
),
(
'Enable the "raw" directive. Enabled by default.',
["--raw-enabled"],
{"action": "store_true"},
),
(
"Token name set for parsing code with Pygments: one of "
'"long", "short", or "none (no parsing)". Default is "long".',
["--syntax-highlight"],
{
"choices": ["long", "short", "none"],
"default": "long",
"metavar": "<format>",
},
),
(
"Change straight quotation marks to typographic form: "
'one of "yes", "no", "alt[ernative]" (default "no").',
["--smart-quotes"],
{
"default": False,
"metavar": "<yes/no/alt>",
"validator": frontend.validate_ternary,
},
),
(
'Characters to use as "smart quotes" for <language>. ',
["--smartquotes-locales"],
{
"metavar": "<language:quotes[,language:quotes,...]>",
"action": "append",
"validator": frontend.validate_smartquotes_locales,
},
),
(
"Inline markup recognized at word boundaries only "
"(adjacent to punctuation or whitespace). "
"Force character-level inline markup recognition with "
'"\\ " (backslash + space). Default.',
["--word-level-inline-markup"],
{"action": "store_false", "dest": "character_level_inline_markup"},
),
(
"Inline markup recognized anywhere, regardless of surrounding "
"characters. Backslash-escapes must be used to avoid unwanted "
"markup recognition. Useful for East Asian languages. "
"Experimental.",
["--character-level-inline-markup"],
{
"action": "store_true",
"default": False,
"dest": "character_level_inline_markup",
},
),
),
)
config_section = "myst parser"
config_section_dependencies = ("parsers",)
[docs] def parse(
self, inputstring: str, document: nodes.document, renderer: str = "sphinx"
):
"""Parse source text.
:param inputstring: The source string to parse
:param document: The root docutils node to add AST elements to
"""
if renderer == "sphinx":
config = document.settings.env.myst_config
else:
config = MdParserConfig()
parser = default_parser(config)
parser.options["document"] = document
env = AttrDict()
tokens = parser.parse(inputstring, env)
if not tokens or tokens[0].type != "front_matter":
# we always add front matter, so that we can merge it with global keys,
# specified in the sphinx configuration
tokens = [Token("front_matter", "", 0, content="{}", map=[0, 0])] + tokens
parser.renderer.render(tokens, parser.options, env)
def parse(app: Sphinx, text: str, docname: str = "index") -> nodes.document:
"""Parse a string as MystMarkdown with Sphinx application."""
app.env.temp_data["docname"] = docname
app.env.all_docs[docname] = time.time()
reader = SphinxStandaloneReader()
reader.setup(app)
parser = MystParser()
parser.set_application(app)
with sphinx_domains(app.env):
return publish_doctree(
text,
path.join(app.srcdir, docname + ".md"),
reader=reader,
parser=parser,
parser_name="markdown",
settings_overrides={"env": app.env, "gettext_compact": True},
)