Skip to content

Commit 1e440e6

Browse files
authored
🔧 Minor improvement to directive parsing code (#741)
1 parent aa1d225 commit 1e440e6

File tree

3 files changed

+90
-28
lines changed

3 files changed

+90
-28
lines changed

myst_parser/mdit_to_docutils/base.py

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -732,26 +732,27 @@ def render_code_block(self, token: SyntaxTreeNode) -> None:
732732
self.current_node.append(node)
733733

734734
def render_fence(self, token: SyntaxTreeNode) -> None:
735-
text = token.content
736-
# Ensure that we'll have an empty string if info exists but is only spaces
737-
info = token.info.strip() if token.info else token.info
738-
language = info.split()[0] if info else ""
735+
"""Render a fenced code block."""
736+
# split the info into possible ```name arguments
737+
parts = (token.info.strip() if token.info else "").split(maxsplit=1)
738+
name = parts[0] if parts else ""
739+
arguments = parts[1] if len(parts) > 1 else ""
739740

740741
if (not self.md_config.commonmark_only) and (not self.md_config.gfm_only):
741-
if language == "{eval-rst}":
742+
if name == "{eval-rst}":
742743
return self.render_restructuredtext(token)
743-
if language.startswith("{") and language.endswith("}"):
744-
return self.render_directive(token)
744+
if name.startswith("{") and name.endswith("}"):
745+
return self.render_directive(token, name[1:-1], arguments)
745746

746-
if not language and self.sphinx_env is not None:
747+
if not name and self.sphinx_env is not None:
747748
# use the current highlight setting, via the ``highlight`` directive,
748749
# or ``highlight_language`` configuration.
749-
language = self.sphinx_env.temp_data.get(
750+
name = self.sphinx_env.temp_data.get(
750751
"highlight_language", self.sphinx_env.config.highlight_language
751752
)
752753

753754
lineno_start = 1
754-
number_lines = language in self.md_config.number_code_blocks
755+
number_lines = name in self.md_config.number_code_blocks
755756
emphasize_lines = (
756757
str(token.attrs.get("emphasize-lines"))
757758
if "emphasize-lines" in token.attrs
@@ -763,8 +764,8 @@ def render_fence(self, token: SyntaxTreeNode) -> None:
763764
number_lines = True
764765

765766
node = self.create_highlighted_code_block(
766-
text,
767-
language,
767+
token.content,
768+
name,
768769
number_lines=number_lines,
769770
lineno_start=lineno_start,
770771
source=self.document["source"],
@@ -1525,10 +1526,11 @@ def render_myst_role(self, token: SyntaxTreeNode) -> None:
15251526
self.current_node += _nodes + messages2
15261527

15271528
def render_colon_fence(self, token: SyntaxTreeNode) -> None:
1528-
"""Render a code fence with ``:`` colon delimiters."""
1529-
1530-
info = token.info.strip() if token.info else token.info
1531-
name = info.split()[0] if info else ""
1529+
"""Render a div block, with ``:`` colon delimiters."""
1530+
# split the info into possible :::name arguments
1531+
parts = (token.info.strip() if token.info else "").split(maxsplit=1)
1532+
name = parts[0] if parts else ""
1533+
arguments = parts[1] if len(parts) > 1 else ""
15321534

15331535
if name.startswith("{") and name.endswith("}"):
15341536
if token.content.startswith(":::"):
@@ -1538,7 +1540,7 @@ def render_colon_fence(self, token: SyntaxTreeNode) -> None:
15381540
linear_token = token.token.copy()
15391541
linear_token.content = "\n" + linear_token.content
15401542
token.token = linear_token
1541-
return self.render_directive(token)
1543+
return self.render_directive(token, name[1:-1], arguments)
15421544

15431545
container = nodes.container(is_div=True)
15441546
self.add_line_and_source_path(container, token)
@@ -1661,18 +1663,26 @@ def render_restructuredtext(self, token: SyntaxTreeNode) -> None:
16611663
self.document.note_explicit_target(node, node)
16621664
self.current_node.extend(newdoc.children)
16631665

1664-
def render_directive(self, token: SyntaxTreeNode) -> None:
1665-
"""Render special fenced code blocks as directives."""
1666-
first_line = token.info.split(maxsplit=1)
1667-
name = first_line[0][1:-1]
1668-
arguments = "" if len(first_line) == 1 else first_line[1]
1669-
content = token.content
1666+
def render_directive(
1667+
self, token: SyntaxTreeNode, name: str, arguments: str
1668+
) -> None:
1669+
"""Render special fenced code blocks as directives.
1670+
1671+
:param token: the token to render
1672+
:param name: the name of the directive
1673+
:param arguments: The remaining text on the same line as the directive name.
1674+
"""
16701675
position = token_line(token)
1671-
nodes_list = self.run_directive(name, arguments, content, position)
1676+
nodes_list = self.run_directive(name, arguments, token.content, position)
16721677
self.current_node += nodes_list
16731678

16741679
def run_directive(
1675-
self, name: str, first_line: str, content: str, position: int
1680+
self,
1681+
name: str,
1682+
first_line: str,
1683+
content: str,
1684+
position: int,
1685+
additional_options: dict[str, str] | None = None,
16761686
) -> list[nodes.Element]:
16771687
"""Run a directive and return the generated nodes.
16781688
@@ -1681,6 +1691,8 @@ def run_directive(
16811691
May be an argument or body text, dependent on the directive
16821692
:param content: All text after the first line. Can include options.
16831693
:param position: The line number of the first line
1694+
:param additional_options: Additional options to add to the directive,
1695+
above those parsed from the content.
16841696
16851697
"""
16861698
self.document.current_line = position
@@ -1706,7 +1718,12 @@ def run_directive(
17061718
directive_class.option_spec["heading-offset"] = directives.nonnegative_int
17071719

17081720
try:
1709-
parsed = parse_directive_text(directive_class, first_line, content)
1721+
parsed = parse_directive_text(
1722+
directive_class,
1723+
first_line,
1724+
content,
1725+
additional_options=additional_options,
1726+
)
17101727
except MarkupError as error:
17111728
error = self.reporter.error(
17121729
f"Directive '{name}': {error}",

myst_parser/parsers/directives.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,28 @@ def parse_directive_text(
6565
directive_class: type[Directive],
6666
first_line: str,
6767
content: str,
68+
*,
6869
validate_options: bool = True,
70+
additional_options: dict[str, str] | None = None,
6971
) -> DirectiveParsingResult:
7072
"""Parse (and validate) the full directive text.
7173
7274
:param first_line: The text on the same line as the directive name.
7375
May be an argument or body text, dependent on the directive
7476
:param content: All text after the first line. Can include options.
7577
:param validate_options: Whether to validate the values of options
78+
:param additional_options: Additional options to add to the directive,
79+
above those parsed from the content (content options take priority).
7680
7781
:raises MarkupError: if there is a fatal parsing/validation error
7882
"""
7983
parse_errors: list[str] = []
8084
if directive_class.option_spec:
8185
body, options, option_errors = parse_directive_options(
82-
content, directive_class, validate=validate_options
86+
content,
87+
directive_class,
88+
validate=validate_options,
89+
additional_options=additional_options,
8390
)
8491
parse_errors.extend(option_errors)
8592
body_lines = body.splitlines()
@@ -114,7 +121,10 @@ def parse_directive_text(
114121

115122

116123
def parse_directive_options(
117-
content: str, directive_class: type[Directive], validate: bool = True
124+
content: str,
125+
directive_class: type[Directive],
126+
validate: bool = True,
127+
additional_options: dict[str, str] | None = None,
118128
) -> tuple[str, dict, list[str]]:
119129
"""Parse (and validate) the directive option section.
120130
@@ -162,6 +172,10 @@ def parse_directive_options(
162172
# but since its for testing only we accept all options
163173
return content, options, validation_errors
164174

175+
if additional_options:
176+
# The YAML block takes priority over additional options
177+
options = {**additional_options, **options}
178+
165179
# check options against spec
166180
options_spec: dict[str, Callable] = directive_class.option_spec
167181
unknown_options: list[str] = []

tests/test_renderers/test_parse_directives.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,34 @@ def test_parsing(file_params):
4949
def test_parsing_errors(descript, klass, arguments, content):
5050
with pytest.raises(MarkupError):
5151
parse_directive_text(klass, arguments, content)
52+
53+
54+
def test_additional_options():
55+
"""Allow additional options to be passed to a directive."""
56+
# this should be fine
57+
result = parse_directive_text(
58+
Note, "", "content", additional_options={"class": "bar"}
59+
)
60+
assert not result.warnings
61+
assert result.options == {"class": ["bar"]}
62+
assert result.body == ["content"]
63+
# body on first line should also be fine
64+
result = parse_directive_text(
65+
Note, "content", "other", additional_options={"class": "bar"}
66+
)
67+
assert not result.warnings
68+
assert result.options == {"class": ["bar"]}
69+
assert result.body == ["content", "other"]
70+
# additional option should not take precedence
71+
result = parse_directive_text(
72+
Note, "content", ":class: foo", additional_options={"class": "bar"}
73+
)
74+
assert not result.warnings
75+
assert result.options == {"class": ["foo"]}
76+
assert result.body == ["content"]
77+
# this should warn about the unknown option
78+
result = parse_directive_text(
79+
Note, "", "content", additional_options={"foo": "bar"}
80+
)
81+
assert len(result.warnings) == 1
82+
assert "Unknown option" in result.warnings[0]

0 commit comments

Comments
 (0)