diff --git a/myst_parser/directives.py b/myst_parser/directives.py index 5616cef8..4cb90306 100644 --- a/myst_parser/directives.py +++ b/myst_parser/directives.py @@ -1,11 +1,14 @@ """MyST specific directives""" -from typing import List, Tuple +from copy import copy +from typing import List, Tuple, cast from docutils import nodes from docutils.parsers.rst import directives from sphinx.directives import SphinxDirective from sphinx.util.docutils import SphinxRole +from myst_parser.mocking import MockState + def align(argument): return directives.choice(argument, ("left", "center", "right")) @@ -61,16 +64,20 @@ def run(self) -> List[nodes.Node]: figclasses = self.options.pop("class", None) align = self.options.pop("align", None) - node = nodes.Element() - # TODO test that we are using myst parser + if not isinstance(self.state, MockState): + return [self.figure_error("Directive is only supported in myst parser")] + state = cast(MockState, self.state) + # ensure html image enabled - myst_extensions = self.state._renderer.config.get("myst_extensions", set()) + myst_extensions = copy(state._renderer.md_config.enable_extensions) + node = nodes.Element() try: - self.state._renderer.config.setdefault("myst_extensions", set()) - self.state._renderer.config["myst_extensions"].add("html_image") - self.state.nested_parse(self.content, self.content_offset, node) + state._renderer.md_config.enable_extensions = list( + state._renderer.md_config.enable_extensions + ) + ["html_image"] + state.nested_parse(self.content, self.content_offset, node) finally: - self.state._renderer.config["myst_extensions"] = myst_extensions + state._renderer.md_config.enable_extensions = myst_extensions if not len(node.children) == 2: return [ diff --git a/myst_parser/docutils_renderer.py b/myst_parser/docutils_renderer.py index 49a21ee0..aac3655e 100644 --- a/myst_parser/docutils_renderer.py +++ b/myst_parser/docutils_renderer.py @@ -8,6 +8,7 @@ from datetime import date, datetime from types import ModuleType from typing import ( + TYPE_CHECKING, Any, Dict, Iterator, @@ -42,6 +43,7 @@ from markdown_it.token import Token from markdown_it.tree import SyntaxTreeNode +from myst_parser.main import MdParserConfig from myst_parser.mocking import ( MockIncludeDirective, MockingError, @@ -54,6 +56,9 @@ from .parse_directives import DirectiveParsingError, parse_directive_text from .utils import is_external_url +if TYPE_CHECKING: + from sphinx.environment import BuildEnvironment + def make_document(source_path="notset", parser_cls=RSTParser) -> nodes.document: """Create a new docutils document, with the parser classes' default settings.""" @@ -94,7 +99,8 @@ def __getattr__(self, name: str): """Warn when the renderer has not been setup yet.""" if name in ( "md_env", - "config", + "md_config", + "md_options", "document", "current_node", "reporter", @@ -113,11 +119,10 @@ def setup_render( ) -> None: """Setup the renderer with per render variables.""" self.md_env = env - self.config: Dict[str, Any] = options - self.document: nodes.document = self.config.get("document", make_document()) - self.current_node: nodes.Element = self.config.get( - "current_node", self.document - ) + self.md_options = options + self.md_config: MdParserConfig = options["myst_config"] + self.document: nodes.document = options.get("document", make_document()) + self.current_node: nodes.Element = options.get("current_node", self.document) self.reporter: Reporter = self.document.reporter # note there are actually two possible language modules: # one from docutils.languages, and one from docutils.parsers.rst.languages @@ -130,7 +135,7 @@ def setup_render( } @property - def sphinx_env(self) -> Optional[Any]: + def sphinx_env(self) -> Optional["BuildEnvironment"]: """Return the sphinx env, if using Sphinx.""" try: return self.document.settings.env @@ -221,9 +226,6 @@ def render( append_to=self.document, ) - if not self.config.get("output_footnotes", True): - return self.document - # we don't use the foot_references stored in the env # since references within directives/roles will have been added after # those from the initial markdown parse @@ -233,7 +235,7 @@ def render( if refnode["refname"] not in foot_refs: foot_refs[refnode["refname"]] = True - if foot_refs and self.config.get("myst_footnote_transition", False): + if foot_refs and self.md_config.footnote_transition: self.current_node.append(nodes.transition(classes=["footnotes"])) for footref in foot_refs: foot_ref_tokens = self.md_env["foot_refs"].get(footref, []) @@ -522,9 +524,7 @@ def create_highlighted_code_block( lex_tokens = Lexer( text, lexer_name or "", - "short" - if self.config.get("myst_highlight_code_blocks", True) - else "none", + "short" if self.md_config.highlight_code_blocks else "none", ) except LexerError as err: self.reporter.warning( @@ -571,7 +571,7 @@ def render_fence(self, token: SyntaxTreeNode) -> None: info = token.info.strip() if token.info else token.info language = info.split()[0] if info else "" - if not self.config.get("commonmark_only", False) and language == "{eval-rst}": + if not self.md_config.commonmark_only and language == "{eval-rst}": # copy necessary elements (source, line no, env, reporter) newdoc = make_document() newdoc["source"] = self.document["source"] @@ -587,7 +587,7 @@ def render_fence(self, token: SyntaxTreeNode) -> None: self.current_node.extend(newdoc[:]) return elif ( - not self.config.get("commonmark_only", False) + not self.md_config.commonmark_only and language.startswith("{") and language.endswith("}") ): @@ -603,7 +603,7 @@ def render_fence(self, token: SyntaxTreeNode) -> None: node = self.create_highlighted_code_block( text, language, - number_lines=language in self.config.get("myst_number_code_blocks", ()), + number_lines=language in self.md_config.number_code_blocks, source=self.document["source"], line=token_line(token, 0) or None, ) @@ -615,7 +615,7 @@ def blocks_mathjax_processing(self) -> bool: return ( self.sphinx_env is not None and "myst_update_mathjax" in self.sphinx_env.config - and self.sphinx_env.config.myst_update_mathjax + and self.md_config.update_mathjax ) def render_heading(self, token: SyntaxTreeNode) -> None: @@ -680,14 +680,14 @@ def render_link(self, token: SyntaxTreeNode) -> None: if token.markup == "autolink": return self.render_autolink(token) - if self.config.get("myst_all_links_external", False): + if self.md_config.all_links_external: return self.render_external_url(token) # Check for external URL url_scheme = urlparse(cast(str, token.attrGet("href") or "")).scheme - allowed_url_schemes = self.config.get("myst_url_schemes", None) + allowed_url_schemes = self.md_config.url_schemes if (allowed_url_schemes is None and url_scheme) or ( - url_scheme in allowed_url_schemes + allowed_url_schemes is not None and url_scheme in allowed_url_schemes ): return self.render_external_url(token) @@ -750,14 +750,14 @@ def render_image(self, token: SyntaxTreeNode) -> None: self.add_line_and_source_path(img_node, token) destination = cast(str, token.attrGet("src") or "") - if self.config.get("relative-images", None) is not None and not is_external_url( + if self.md_env.get("relative-images", None) is not None and not is_external_url( destination, None, True ): # make the path relative to an "including" document # this is set when using the `relative-images` option of the MyST `include` directive destination = os.path.normpath( os.path.join( - self.config.get("relative-images", ""), + self.md_env.get("relative-images", ""), os.path.normpath(destination), ) ) @@ -822,7 +822,7 @@ def render_front_matter(self, token: SyntaxTreeNode) -> None: self.current_node.extend( html_meta_to_nodes( { - **self.config.get("myst_html_meta", {}), + **self.md_config.html_meta, **html_meta, }, document=self.document, @@ -1287,8 +1287,8 @@ def render_substitution(self, token: SyntaxTreeNode, inline: bool) -> None: position = token_line(token) # front-matter substitutions take priority over config ones - variable_context = { - **self.config.get("myst_substitutions", {}), + variable_context: Dict[str, Any] = { + **self.md_config.substitutions, **getattr(self.document, "fm_substitutions", {}), } if self.sphinx_env is not None: diff --git a/myst_parser/html_to_nodes.py b/myst_parser/html_to_nodes.py index 1576a216..d6bd7b0b 100644 --- a/myst_parser/html_to_nodes.py +++ b/myst_parser/html_to_nodes.py @@ -35,10 +35,8 @@ def html_to_nodes( text: str, line_number: int, renderer: "DocutilsRenderer" ) -> List[nodes.Element]: """Convert HTML to docutils nodes.""" - enable_html_img = "html_image" in renderer.config.get("myst_extensions", []) - enable_html_admonition = "html_admonition" in renderer.config.get( - "myst_extensions", [] - ) + enable_html_img = "html_image" in renderer.md_config.enable_extensions + enable_html_admonition = "html_admonition" in renderer.md_config.enable_extensions if not (enable_html_img or enable_html_admonition): return default_html(text, renderer.document["source"], line_number) diff --git a/myst_parser/main.py b/myst_parser/main.py index 30f98cc6..5994855d 100644 --- a/myst_parser/main.py +++ b/myst_parser/main.py @@ -40,7 +40,7 @@ class MdParserConfig: validator=instance_of(bool), metadata={"help": "Use strict CommonMark parser"}, ) - enable_extensions: Iterable[str] = attr.ib( + enable_extensions: Sequence[str] = attr.ib( factory=lambda: ["dollarmath"], metadata={"help": "Enable extensions"} ) @@ -232,7 +232,7 @@ def create_md_parser( md = MarkdownIt("commonmark", renderer_cls=renderer).use( wordcount_plugin, per_minute=config.words_per_minute ) - md.options.update({"commonmark_only": True}) + md.options.update({"myst_config": config}) return md md = ( @@ -291,22 +291,9 @@ def create_md_parser( md.options.update( { - # standard options "typographer": typographer, "linkify": "linkify" in config.enable_extensions, - # myst options - "commonmark_only": False, - "myst_extensions": set( - list(config.enable_extensions) - + (["heading_anchors"] if config.heading_anchors is not None else []) - ), - "myst_all_links_external": config.all_links_external, - "myst_url_schemes": config.url_schemes, - "myst_substitutions": config.substitutions, - "myst_html_meta": config.html_meta, - "myst_footnote_transition": config.footnote_transition, - "myst_number_code_blocks": config.number_code_blocks, - "myst_highlight_code_blocks": config.highlight_code_blocks, + "myst_config": config, } ) diff --git a/myst_parser/mocking.py b/myst_parser/mocking.py index 176704d9..68a1414a 100644 --- a/myst_parser/mocking.py +++ b/myst_parser/mocking.py @@ -441,11 +441,11 @@ def run(self) -> List[nodes.Element]: self.renderer.reporter.source = str(path) self.renderer.reporter.get_source_and_line = lambda l: (str(path), l) if "relative-images" in self.options: - self.renderer.config["relative-images"] = os.path.relpath( + self.renderer.md_env["relative-images"] = os.path.relpath( path.parent, source_dir ) if "relative-docs" in self.options: - self.renderer.config["relative-docs"] = ( + self.renderer.md_env["relative-docs"] = ( self.options["relative-docs"], source_dir, path.parent, @@ -456,8 +456,8 @@ def run(self) -> List[nodes.Element]: finally: self.renderer.document["source"] = source self.renderer.reporter.source = rsource - self.renderer.config.pop("relative-images", None) - self.renderer.config.pop("relative-docs", None) + self.renderer.md_env.pop("relative-images", None) + self.renderer.md_env.pop("relative-docs", None) if line_func is not None: self.renderer.reporter.get_source_and_line = line_func else: diff --git a/myst_parser/sphinx_renderer.py b/myst_parser/sphinx_renderer.py index cecbc907..5b3a175a 100644 --- a/myst_parser/sphinx_renderer.py +++ b/myst_parser/sphinx_renderer.py @@ -74,7 +74,7 @@ def render_internal_link(self, token: SyntaxTreeNode) -> None: # make the path relative to an "including" document # this is set when using the `relative-docs` option of the MyST `include` directive - relative_include = self.config.get("relative-docs", None) + relative_include = self.md_env.get("relative-docs", None) if relative_include is not None and destination.startswith(relative_include[0]): source_dir, include_dir = relative_include[1:] destination = os.path.relpath( diff --git a/tests/test_html/test_html_to_nodes.py b/tests/test_html/test_html_to_nodes.py index 62384305..c43d0a1c 100644 --- a/tests/test_html/test_html_to_nodes.py +++ b/tests/test_html/test_html_to_nodes.py @@ -3,9 +3,10 @@ import pytest from docutils import nodes -from markdown_it.utils import read_fixture_file +from pytest_param_files import with_parameters from myst_parser.html_to_nodes import html_to_nodes +from myst_parser.main import MdParserConfig FIXTURE_PATH = Path(__file__).parent @@ -18,7 +19,7 @@ def _run_directive(name: str, first_line: str, content: str, position: int): return [node] return Mock( - config={"myst_extensions": ["html_image", "html_admonition"]}, + md_config=MdParserConfig(enable_extensions=["html_image", "html_admonition"]), document={"source": "source"}, reporter=Mock( warning=Mock(return_value=nodes.system_message("warning")), @@ -28,18 +29,8 @@ def _run_directive(name: str, first_line: str, content: str, position: int): ) -@pytest.mark.parametrize( - "line,title,text,expected", - read_fixture_file(FIXTURE_PATH / "html_to_nodes.md"), - ids=[ - f"{i[0]}-{i[1]}" for i in read_fixture_file(FIXTURE_PATH / "html_to_nodes.md") - ], -) -def test_html_to_nodes(line, title, text, expected, mock_renderer): +@with_parameters(FIXTURE_PATH / "html_to_nodes.md") +def test_html_to_nodes(file_params, mock_renderer): output = nodes.container() - output += html_to_nodes(text, line_number=0, renderer=mock_renderer) - try: - assert output.pformat().rstrip() == expected.rstrip() - except AssertionError: - print(output.pformat()) - raise + output += html_to_nodes(file_params.content, line_number=0, renderer=mock_renderer) + file_params.assert_expected(output.pformat(), rstrip=True) diff --git a/tests/test_html/test_parse_html.py b/tests/test_html/test_parse_html.py index ff9bbaf4..a9775414 100644 --- a/tests/test_html/test_parse_html.py +++ b/tests/test_html/test_parse_html.py @@ -1,41 +1,24 @@ from pathlib import Path -import pytest -from markdown_it.utils import read_fixture_file +from pytest_param_files import with_parameters from myst_parser.parse_html import tokenize_html FIXTURE_PATH = Path(__file__).parent -@pytest.mark.parametrize( - "line,title,text,expected", - read_fixture_file(FIXTURE_PATH / "html_ast.md"), - ids=[f"{i[0]}-{i[1]}" for i in read_fixture_file(FIXTURE_PATH / "html_ast.md")], -) -def test_html_ast(line, title, text, expected): - tokens = "\n".join(repr(t) for t in tokenize_html(text).walk(include_self=True)) - try: - assert tokens.rstrip() == expected.rstrip() - except AssertionError: - print(tokens) - raise - - -@pytest.mark.parametrize( - "line,title,text,expected", - read_fixture_file(FIXTURE_PATH / "html_round_trip.md"), - ids=[ - f"{i[0]}-{i[1]}" for i in read_fixture_file(FIXTURE_PATH / "html_round_trip.md") - ], -) -def test_html_round_trip(line, title, text, expected): - ast = tokenize_html(text) - try: - assert str(ast).rstrip() == expected.rstrip() - except AssertionError: - print(str(ast)) - raise +@with_parameters(FIXTURE_PATH / "html_ast.md") +def test_html_ast(file_params): + tokens = "\n".join( + repr(t) for t in tokenize_html(file_params.content).walk(include_self=True) + ) + file_params.assert_expected(tokens, rstrip=True) + + +@with_parameters(FIXTURE_PATH / "html_round_trip.md") +def test_html_round_trip(file_params): + ast = tokenize_html(file_params.content) + file_params.assert_expected(str(ast), rstrip=True) def test_render_overrides():