Skip to content

Commit

Permalink
♻️ REFACTOR: Add MdParserConfig class
Browse files Browse the repository at this point in the history
This centralises parsing configuration storage and validation, meaning it only has to be updated in a single place.
Also moved from using `texmath_plugin` to `dollarmath_plugin`
  • Loading branch information
chrisjsewell committed Aug 18, 2020
1 parent 9c4ddac commit 98573b9
Show file tree
Hide file tree
Showing 16 changed files with 159 additions and 121 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@

myst_amsmath_enable = True
myst_admonition_enable = True
myst_html_img = True
myst_html_img_enable = True


def run_apidoc(app):
Expand Down
2 changes: 1 addition & 1 deletion docs/using/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ The final option is directly using HTML, which is also parsed by MyST.
This is usually a bad option, because the HTML is treated as raw text during the build process and so sphinx will not recognise that the image file is to be copied, and will not output the HTML into non-HTML output formats.
HTML parsing to the rescue!
By setting `myst_html_img = True` in the sphinx `conf.py` configuration file, MySt-Parser will attempt to convert any isolated `img` tags (i.e. not wrapped in any other HTML) to the internal representation used in sphinx.
By setting `myst_html_img_enable = True` in the sphinx `conf.py` configuration file, MySt-Parser will attempt to convert any isolated `img` tags (i.e. not wrapped in any other HTML) to the internal representation used in sphinx.
```md
<img src="img/fun-fish.png" alt="fishy" class="bg-primary" width="200px">
Expand Down
46 changes: 19 additions & 27 deletions myst_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,34 @@ def setup_sphinx(app):
# so that it can be called by external packages like myst_nb
from myst_parser.myst_refs import MystReferenceResolver
from myst_parser.myst_amsmath import MystAmsMathTransform
from myst_parser.main import MdParserConfig

app.add_post_transform(MystReferenceResolver)
app.add_post_transform(MystAmsMathTransform)

app.add_config_value("myst_disable_syntax", (), "env")
# see https://en.wikipedia.org/wiki/List_of_URI_schemes
app.add_config_value("myst_url_schemes", None, "env")
app.add_config_value("myst_math_delimiters", "dollars", "env")
app.add_config_value("myst_amsmath_enable", False, "env")
app.add_config_value("myst_admonition_enable", False, "env")
app.add_config_value("myst_html_img", False, "env")
for name, default in MdParserConfig().as_dict().items():
if not name == "renderer":
app.add_config_value(f"myst_{name}", default, "env")

app.connect("config-inited", validate_config)
app.connect("builder-inited", create_myst_config)


def validate_config(app, config):
def create_myst_config(app):
from sphinx.util import logging
from sphinx.util.console import bold
from myst_parser.main import MdParserConfig

logger = logging.getLogger(__name__)

# TODO raise errors or log error with sphinx?
values = {
name: app.config[f"myst_{name}"]
for name in MdParserConfig().as_dict().keys()
if name != "renderer"
}

try:
for s in config.myst_disable_syntax:
assert isinstance(s, str)
except (AssertionError, TypeError):
logger.error("myst_disable_syntax config option not of type List[str]")

allowed_delimiters = ["brackets", "kramdown", "dollars", "julia"]
if config.myst_math_delimiters not in allowed_delimiters:
logger.error(
"myst_math_delimiters config option not an allowed name: "
+ f"{allowed_delimiters}"
)

if not isinstance(config.myst_amsmath_enable, bool):
logger.error("myst_amsmath_enable config option not of type boolean")

if not isinstance(config.myst_admonition_enable, bool):
logger.error("myst_admonition_enable config option not of type boolean")
app.env.myst_config = MdParserConfig(**values)
logger.info(bold("myst v%s:") + " %s", __version__, app.env.myst_config)
except (TypeError, ValueError) as error:
logger.error("myst configuration invalid: %s", error.args[0])
app.env.myst_config = MdParserConfig()
9 changes: 4 additions & 5 deletions myst_parser/cli/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,16 @@ def run_myst_parser_html(package, text):
@benchmark("myst_parser.main")
def run_myst_parser_docutils(package, text):
package.to_docutils(
text, renderer="docutils", options={"ignore_missing_refs": True}
text,
package.MdParserConfig(renderer="docutils"),
options={"ignore_missing_refs": True},
)


@benchmark("myst_parser.main")
def run_myst_parser_sphinx(package, text):
package.to_docutils(
text,
renderer="sphinx",
options={"ignore_missing_refs": True},
in_sphinx_env=True,
text, options={"ignore_missing_refs": True}, in_sphinx_env=True,
)


Expand Down
2 changes: 1 addition & 1 deletion myst_parser/docutils_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def render_html_inline(self, token):

def render_html_block(self, token):
node = None
if self.config.get("myst_html_img", False):
if self.config.get("enable_html_img", False):
node = HTMLImgParser().parse(token.content, self.document, token.map[0])
if node is None:
node = nodes.raw("", token.content, format="html")
Expand Down
110 changes: 74 additions & 36 deletions myst_parser/main.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,113 @@
from typing import List
from typing import List, Optional

import attr
from attr.validators import deep_iterable, in_, instance_of, optional

from markdown_it import MarkdownIt
from markdown_it.renderer import RendererHTML
from markdown_it.extensions.front_matter import front_matter_plugin
from markdown_it.extensions.myst_blocks import myst_block_plugin
from markdown_it.extensions.myst_role import myst_role_plugin
from markdown_it.extensions.texmath import texmath_plugin

# from markdown_it.extensions.texmath import texmath_plugin
from markdown_it.extensions.dollarmath import dollarmath_plugin
from markdown_it.extensions.footnote import footnote_plugin
from markdown_it.extensions.amsmath import amsmath_plugin
from markdown_it.extensions.container import container_plugin

from . import __version__ # noqa: F401


def default_parser(
renderer="sphinx",
disable_syntax=(),
math_delimiters: str = "dollars",
enable_amsmath: bool = False,
enable_admonitions: bool = False,
) -> MarkdownIt:
@attr.s()
class MdParserConfig:
"""Configuration options for the Markdown Parser."""

renderer: str = attr.ib(
default="sphinx", validator=in_(["sphinx", "html", "docutils"])
)
dmath_enable: bool = attr.ib(default=True, validator=instance_of(bool))
dmath_allow_labels: bool = attr.ib(default=True, validator=instance_of(bool))
dmath_allow_space: bool = attr.ib(default=True, validator=instance_of(bool))
dmath_allow_digits: bool = attr.ib(default=True, validator=instance_of(bool))

amsmath_enable: bool = attr.ib(default=False, validator=instance_of(bool))
admonitions_enable: bool = attr.ib(default=False, validator=instance_of(bool))

disable_syntax: List[str] = attr.ib(
factory=list,
validator=deep_iterable(instance_of(str), instance_of((list, tuple))),
)

html_img_enable: bool = attr.ib(default=False, validator=instance_of(bool))

# see https://en.wikipedia.org/wiki/List_of_URI_schemes
url_schemes: Optional[List[str]] = attr.ib(
default=None,
validator=optional(deep_iterable(instance_of(str), instance_of((list, tuple)))),
)

def as_dict(self, dict_factory=dict) -> dict:
return attr.asdict(self, dict_factory=dict_factory)


def default_parser(config: MdParserConfig) -> MarkdownIt:
"""Return the default parser configuration for MyST"""
if renderer == "sphinx":
if config.renderer == "sphinx":
from myst_parser.sphinx_renderer import SphinxRenderer

renderer_cls = SphinxRenderer
elif renderer == "html":
elif config.renderer == "html":
renderer_cls = RendererHTML
elif renderer == "docutils":
elif config.renderer == "docutils":
from myst_parser.docutils_renderer import DocutilsRenderer

renderer_cls = DocutilsRenderer
else:
raise ValueError("unknown renderer type: {0}".format(renderer))
raise ValueError("unknown renderer type: {0}".format(config.renderer))

md = (
MarkdownIt("commonmark", renderer_cls=renderer_cls)
.enable("table")
.use(front_matter_plugin)
.use(myst_block_plugin)
.use(myst_role_plugin)
.use(texmath_plugin, delimiters=math_delimiters)
.use(footnote_plugin)
.disable("footnote_inline")
# disable this for now, because it need a new implementation in the renderer
.disable("footnote_tail")
)
if config.dmath_enable:
md.use(
dollarmath_plugin,
allow_labels=config.dmath_allow_labels,
allow_space=config.dmath_allow_space,
allow_digits=config.dmath_allow_digits,
)
if config.admonitions_enable:
# we don't want to yet remove un-referenced, because they may be referenced
# in admonition type directives
# so we do our own post processing
)
for name in disable_syntax:
md.disable(name, True)
if enable_admonitions:
md.use(container_plugin, "admonition", validate=validate_admonition)
if enable_amsmath:
if config.amsmath_enable:
md.use(amsmath_plugin)
for name in config.disable_syntax:
md.disable(name, True)

md.options.update(
{
"enable_html_img": config.html_img_enable,
"myst_url_schemes": config.url_schemes,
}
)

return md


def to_docutils(
text: str,
parser_config: Optional[MdParserConfig] = None,
options=None,
env=None,
disable_syntax: List[str] = (),
enable_amsmath: bool = False,
enable_admonitions: bool = False,
math_delimiters: str = "dollars",
renderer="sphinx",
document=None,
in_sphinx_env: bool = False,
conf=None,
Expand All @@ -78,7 +119,6 @@ def to_docutils(
:param options: options to update the parser with
:param env: The sandbox environment for the parse
(will contain e.g. reference definitions)
:param disable_syntax: list of syntax element names to disable
:param document: the docutils root node to use (otherwise a new one will be created)
:param in_sphinx_env: initialise a minimal sphinx environment (useful for testing)
:param conf: the sphinx conf.py as a dictionary
Expand All @@ -88,13 +128,7 @@ def to_docutils(
"""
from myst_parser.docutils_renderer import make_document

md = default_parser(
renderer=renderer,
disable_syntax=disable_syntax,
math_delimiters=math_delimiters,
enable_amsmath=enable_amsmath,
enable_admonitions=enable_admonitions,
)
md = default_parser(parser_config or MdParserConfig())
if options:
md.options.update(options)
md.options["document"] = document or make_document()
Expand All @@ -107,13 +141,17 @@ def to_docutils(
return md.render(text, env)


def to_html(text: str, env=None):
md = default_parser("html")
def to_html(text: str, env=None, config: Optional[MdParserConfig] = None):
config = config or MdParserConfig()
config.renderer = "html"
md = default_parser(config)
return md.render(text, env)


def to_tokens(text: str, env=None):
md = default_parser("html")
def to_tokens(text: str, env=None, config: Optional[MdParserConfig] = None):
config = config or MdParserConfig()
config.renderer = "html"
md = default_parser(config)
return md.parse(text, env)


Expand Down
35 changes: 5 additions & 30 deletions myst_parser/sphinx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sphinx.util import logging
from sphinx.util.docutils import sphinx_domains

from myst_parser.main import default_parser
from myst_parser.main import default_parser, MdParserConfig


SPHINX_LOGGER = logging.getLogger(__name__)
Expand All @@ -21,14 +21,7 @@ class MystParser(Parser):
supported = ("md", "markdown", "myst")
translate_section_name = None

default_config = {
"myst_disable_syntax": (),
"myst_url_schemes": None,
"myst_math_delimiters": "dollars",
"myst_amsmath_enable": False,
"myst_admonition_enable": False,
"myst_html_img": False,
}
default_config = {}

# these specs are copied verbatim from the docutils RST parser
settings_spec = (
Expand Down Expand Up @@ -186,31 +179,13 @@ def parse(
:param document: The root docutils node to add AST elements to
"""
if renderer == "sphinx":
config = document.settings.env.app.config
config = document.settings.env.myst_config
else:
config = self.default_config.copy()
parser = self.get_markdown_parser(config, renderer)
config = MdParserConfig()
parser = default_parser(config)
parser.options["document"] = document
parser.render(inputstring)

@staticmethod
def get_markdown_parser(config: dict, renderer: str = "sphinx"):

parser = default_parser(
renderer=renderer,
disable_syntax=config["myst_disable_syntax"],
math_delimiters=config["myst_math_delimiters"],
enable_amsmath=config["myst_amsmath_enable"],
enable_admonitions=config["myst_admonition_enable"],
)
parser.options.update(
{
"myst_url_schemes": config["myst_url_schemes"],
"myst_html_img": config["myst_html_img"],
}
)
return parser


def parse(app: Sphinx, text: str, docname: str = "index") -> nodes.document:
"""Parse a string as MystMarkdown with Sphinx application."""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
keywords="markdown lexer parser development docutils sphinx",
python_requires=">=3.6",
install_requires=[
"markdown-it-py~=0.4.9",
"markdown-it-py~=0.5.0",
"pyyaml",
"docutils>=0.15",
"sphinx>=2,<4",
Expand Down
4 changes: 2 additions & 2 deletions tests/test_renderers/test_error_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from markdown_it.utils import read_fixture_file
from myst_parser.docutils_renderer import make_document
from myst_parser.main import to_docutils
from myst_parser.main import to_docutils, MdParserConfig


FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
Expand All @@ -23,5 +23,5 @@ def observer(msg_node):
messages.append(msg_node.astext())

document.reporter.attach_observer(observer)
to_docutils(input, document=document, renderer="docutils")
to_docutils(input, MdParserConfig(renderer="docutils"), document=document)
assert "\n".join(messages).rstrip() == expected.rstrip()
Loading

0 comments on commit 98573b9

Please sign in to comment.