diff --git a/docs/sphinx/use.md b/docs/sphinx/use.md index 8b6df5df..1678d522 100644 --- a/docs/sphinx/use.md +++ b/docs/sphinx/use.md @@ -185,7 +185,7 @@ All myst-parser warnings are prepended by their type, e.g. to suppress: ``` ``` -WARNING: Non-consecutive header level increase; 1 to 3 [myst.header] +WARNING: Non-consecutive header level increase; H1 to H3 [myst.header] ``` Add to your `conf.py`: 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 4c7340eb..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.""" @@ -90,26 +95,47 @@ def __init__(self, parser: MarkdownIt) -> None: if k.startswith("render_") and k != "render_children" } + def __getattr__(self, name: str): + """Warn when the renderer has not been setup yet.""" + if name in ( + "md_env", + "md_config", + "md_options", + "document", + "current_node", + "reporter", + "language_module_rst", + "_level_to_elem", + ): + raise AttributeError( + f"'{name}' attribute is not available until setup_render() is called" + ) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + def setup_render( self, options: Dict[str, Any], env: MutableMapping[str, Any] ) -> 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 self.language_module_rst: ModuleType = get_language_rst( self.document.settings.language_code ) - self._level_to_elem: Dict[int, nodes.Element] = {0: self.document} + # a mapping of heading levels to its currently associated node + self._level_to_elem: Dict[int, Union[nodes.document, nodes.section]] = { + 0: self.document + } @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 @@ -131,7 +157,7 @@ def create_warning( to handle suppressed warning types. """ kwargs = {"line": line} if line is not None else {} - msg_node = self.reporter.warning(message, **kwargs) + msg_node = self.reporter.warning(f"{message} [{wtype}.{subtype}]", **kwargs) if append_to is not None: append_to.append(msg_node) return msg_node @@ -200,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 @@ -212,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, []) @@ -261,25 +284,36 @@ def add_document_wordcount(self) -> None: substitution_node["names"].append(f"wordcount-{key}") self.document.note_substitution_def(substitution_node, f"wordcount-{key}") - def nested_render_text(self, text: str, lineno: int) -> None: - """Render unparsed text. + def nested_render_text( + self, text: str, lineno: int, inline: bool = False, allow_headings: bool = True + ) -> None: + """Render unparsed text (appending to the current node). :param text: the text to render :param lineno: the starting line number of the text, within the full source + :param inline: whether the text is inline or block + :param allow_headings: whether to allow headings in the text """ + if inline: + tokens = self.md.parseInline(text, self.md_env) + else: + tokens = self.md.parse(text + "\n", self.md_env) - tokens = self.md.parse(text + "\n", self.md_env) + # remove front matter, if present, e.g. from included documents + if tokens and tokens[0].type == "front_matter": + tokens.pop(0) # update the line numbers for token in tokens: if token.map: token.map = [token.map[0] + lineno, token.map[1] + lineno] - # remove front matter - if tokens and tokens[0].type == "front_matter": - tokens.pop(0) - - self._render_tokens(tokens) + current_match_titles = self.md_env.get("match_titles", None) + try: + self.md_env["match_titles"] = allow_headings + self._render_tokens(tokens) + finally: + self.md_env["match_titles"] = current_match_titles @contextmanager def current_node_context( @@ -294,6 +328,7 @@ def current_node_context( self.current_node = current_node def render_children(self, token: SyntaxTreeNode) -> None: + """Render the children of a token.""" for child in token.children or []: if f"render_{child.type}" in self.rules: self.rules[f"render_{child.type}"](child) @@ -324,31 +359,35 @@ def add_line_and_source_path_r( for child in node.traverse(): self.add_line_and_source_path(child, token) - def is_section_level(self, level, section): - return self._level_to_elem.get(level, None) == section - - def add_section(self, section, level): + def update_section_level_state(self, section: nodes.section, level: int) -> None: + """Update the section level state, with the new current section and level.""" + # find the closest parent section parent_level = max( section_level for section_level in self._level_to_elem if level > section_level ) + parent = self._level_to_elem[parent_level] + # if we are jumping up to a non-consecutive level, + # then warn about this, since this will not be propagated in the docutils AST if (level > parent_level) and (parent_level + 1 != level): + msg = f"Non-consecutive header level increase; H{parent_level} to H{level}" + if parent_level == 0: + msg = f"Document headings start at H{level}, not H1" self.create_warning( - "Non-consecutive header level increase; {} to {}".format( - parent_level, level - ), + msg, line=section.line, subtype="header", append_to=self.current_node, ) - parent = self._level_to_elem[parent_level] + # append the new section to the parent parent.append(section) + # update the state for this section level self._level_to_elem[level] = section - # Prune level to limit + # Remove all descendant sections from the section level state self._level_to_elem = { section_level: section for section_level, section in self._level_to_elem.items() @@ -485,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( @@ -534,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"] @@ -550,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("}") ): @@ -566,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, ) @@ -578,10 +615,11 @@ 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: + """Render a heading, e.g. `# Heading`.""" if self.md_env.get("match_titles", None) is False: # this can occur if a nested parse is performed by a directive @@ -599,35 +637,35 @@ def render_heading(self, token: SyntaxTreeNode) -> None: self.render_children(token) return - # Test if we're replacing a section level first level = int(token.tag[1]) - if isinstance(self.current_node, nodes.section): - if self.is_section_level(level, self.current_node): - self.current_node = cast(nodes.Element, self.current_node.parent) - - title_node = nodes.title(token.children[0].content if token.children else "") - self.add_line_and_source_path(title_node, token) + # create the section node new_section = nodes.section() + self.add_line_and_source_path(new_section, token) + # if a top level section, + # then add classes to set default mathjax processing to false + # we then turn it back on, on a per-node basis if level == 1 and self.blocks_mathjax_processing: new_section["classes"].extend(["tex2jax_ignore", "mathjax_ignore"]) - self.add_line_and_source_path(new_section, token) - new_section.append(title_node) - self.add_section(new_section, level) + # update the state of the section levels + self.update_section_level_state(new_section, level) - self.current_node = title_node - self.render_children(token) + # create the title for this section + title_node = nodes.title(token.children[0].content if token.children else "") + self.add_line_and_source_path(title_node, token) + new_section.append(title_node) + # render the heading children into the title + with self.current_node_context(title_node): + self.render_children(token) - assert isinstance(self.current_node, nodes.title) - text = self.current_node.astext() - # if self.translate_section_name: - # text = self.translate_section_name(text) - name = nodes.fully_normalize_name(text) - section = cast(nodes.section, self.current_node.parent) - section["names"].append(name) - self.document.note_implicit_target(section, section) - self.current_node = section + # create a target reference for the section, based on the heading text + name = nodes.fully_normalize_name(title_node.astext()) + new_section["names"].append(name) + self.document.note_implicit_target(new_section, new_section) + + # set the section as the current node for subsequent rendering + self.current_node = new_section def render_link(self, token: SyntaxTreeNode) -> None: """Parse `` or `[text](link "title")` syntax to docutils AST: @@ -642,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) @@ -712,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), ) ) @@ -784,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, @@ -837,26 +875,23 @@ def dict_to_fm_field_list( field_list.source, field_list.line = self.document["source"], line bibliofields = get_language(language_code).bibliographic_fields - state_machine = MockStateMachine(self, line) - state = MockState(self, state_machine, line) for key, value in data.items(): if not isinstance(value, (str, int, float, date, datetime)): value = json.dumps(value) value = str(value) + body = nodes.paragraph() + body.source, body.line = self.document["source"], line if key in bibliofields: - para_nodes, _ = state.inline_text(value, line) + with self.current_node_context(body): + self.nested_render_text(value, line, inline=True) else: - para_nodes = [nodes.literal(value, value)] - - body_children = [nodes.paragraph("", "", *para_nodes)] - body_children[0].source = self.document["source"] - body_children[0].line = 0 + body += nodes.literal(value, value) field_node = nodes.field() field_node.source = value field_node += nodes.field_name(key, "", nodes.Text(key, key)) - field_node += nodes.field_body(value, *body_children) + field_node += nodes.field_body(value, *[body]) field_list += field_node return field_list @@ -1252,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: @@ -1290,10 +1325,6 @@ def render_substitution(self, token: SyntaxTreeNode, inline: bool) -> None: self.current_node += [error_msg] return - # parse rendered text - state_machine = MockStateMachine(self, position) - state = MockState(self, state_machine, position) - # TODO improve error reporting; # at present, for a multi-line substitution, # an error may point to a line lower than the substitution @@ -1302,23 +1333,14 @@ def render_substitution(self, token: SyntaxTreeNode, inline: bool) -> None: # we record used references before nested parsing, then remove them after self.document.sub_references.update(references) - try: if inline and not REGEX_DIRECTIVE_START.match(rendered): - sub_nodes, _ = state.inline_text(rendered, position) + self.nested_render_text(rendered, position, inline=True) else: - base_node = nodes.Element() - state.nested_parse( - StringList(rendered.splitlines(), self.document["source"]), - 0, - base_node, - ) - sub_nodes = base_node.children + self.nested_render_text(rendered, position, allow_headings=False) finally: self.document.sub_references.difference_update(references) - self.current_node.extend(sub_nodes) - def html_meta_to_nodes( data: Dict[str, Any], document: nodes.document, line: int, reporter: Reporter 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 b562e144..68a1414a 100644 --- a/myst_parser/mocking.py +++ b/myst_parser/mocking.py @@ -144,17 +144,13 @@ def nested_parse( since nested heading would break the document structure) """ sm_match_titles = self.state_machine.match_titles - render_match_titles = self._renderer.md_env.get("match_titles", None) - self.state_machine.match_titles = self._renderer.md_env[ - "match_titles" - ] = match_titles - with self._renderer.current_node_context(node): self._renderer.nested_render_text( - "\n".join(block), self._lineno + input_offset + "\n".join(block), + self._lineno + input_offset, + allow_headings=match_titles, ) self.state_machine.match_titles = sm_match_titles - self._renderer.md_env["match_titles"] = render_match_titles def parse_target(self, block, block_text, lineno: int): """ @@ -174,33 +170,14 @@ def inline_text( ) -> Tuple[List[nodes.Element], List[nodes.Element]]: """Parse text with only inline rules. - :return: (list of nodes, list of messages) - + :returns: (list of nodes, list of messages) """ + container = nodes.Element() + with self._renderer.current_node_context(container): + self._renderer.nested_render_text(text, lineno, inline=True) + # TODO return messages? - messages: List[nodes.Element] = [] - paragraph = nodes.paragraph("") - - tokens = self._renderer.md.parseInline(text, self._renderer.md_env) - for token in tokens: - if token.map: - token.map = [token.map[0] + lineno, token.map[1] + lineno] - - # here we instantiate a new renderer, - # so that the nested parse does not effect the current renderer, - # but we use the same env, so that link references, etc - # are added to the global parse. - nested_renderer = self._renderer.__class__(self._renderer.md) - options = {k: v for k, v in self._renderer.config.items()} - options.update( - { - "document": self.document, - "current_node": paragraph, - "output_footnotes": False, - } - ) - nested_renderer.render(tokens, options, self._renderer.md_env) - return paragraph.children, messages + return container.children, [] # U+2014 is an em-dash: attribution_pattern = re.compile("^((?:---?(?!-)|\u2014) *)(.+)") @@ -464,21 +441,23 @@ 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, ) - self.renderer.nested_render_text(file_content, startline + 1) + self.renderer.nested_render_text( + file_content, startline + 1, allow_headings=True + ) 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(): diff --git a/tests/test_renderers/fixtures/reporter_warnings.md b/tests/test_renderers/fixtures/reporter_warnings.md index 3cbe211d..22b53329 100644 --- a/tests/test_renderers/fixtures/reporter_warnings.md +++ b/tests/test_renderers/fixtures/reporter_warnings.md @@ -3,7 +3,7 @@ Duplicate Reference definitions: [a]: b [a]: c . -:2: (WARNING/2) Duplicate reference definition: A +:2: (WARNING/2) Duplicate reference definition: A [myst.ref] . Missing Reference: @@ -77,12 +77,19 @@ x :2: (ERROR/3) Invalid context: the "date" directive can only be used within a substitution definition. . +Do not start headings at H1: +. +## title 1 +. +:1: (WARNING/2) Document headings start at H2, not H1 [myst.header] +. + Non-consecutive headings: . # title 1 ### title 3 . -:2: (WARNING/2) Non-consecutive header level increase; 1 to 3 +:2: (WARNING/2) Non-consecutive header level increase; H1 to H3 [myst.header] . multiple footnote definitions @@ -92,7 +99,7 @@ multiple footnote definitions [^a]: definition 1 [^a]: definition 2 . -:: (WARNING/2) Multiple footnote definitions found for label: 'a' +:: (WARNING/2) Multiple footnote definitions found for label: 'a' [myst.footnote] . Warnings in eval-rst @@ -136,7 +143,7 @@ header nested in admonition # Header ``` . -:2: (WARNING/2) Disallowed nested header found, converting to rubric +:2: (WARNING/2) Disallowed nested header found, converting to rubric [myst.nested_header] . nested parse warning