Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: executablebooks/MyST-Parser
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.19.1
Choose a base ref
...
head repository: executablebooks/MyST-Parser
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.19.2
Choose a head ref
  • 5 commits
  • 9 files changed
  • 3 contributors

Commits on Mar 6, 2023

  1. Copy the full SHA
    2afac09 View commit details
  2. Copy the full SHA
    aa1d225 View commit details

Commits on Mar 7, 2023

  1. Copy the full SHA
    1e440e6 View commit details
  2. Copy the full SHA
    8508bf7 View commit details
  3. Copy the full SHA
    f4afeef View commit details
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## 0.19.2 - 2023-03-02

✨ NEW: Add myst_fence_as_directive config (<gh-pr:742>)

Setting the following config, for example:

```python
extensions = ["myst_parser", "sphinxcontrib.mermaid"]
myst_fence_as_directive = ["mermaid"]
# optional to use directive options
myst_enable_extensions = ["attrs_block"]
```

allows for one to write:

````markdown
{caption="My caption"}
{alt="HTML alt" align=center}
```mermaid
graph LR
a --> b
```
````

and have interoperable rendering with tools like GitHub.

🎉 New contributors:

- 📚 Add `html_last_updated_fmt = ""` to conf.py to fix documentation footer, thanks to <gh-user:jeanas> (<gh-pr:691>)
- 📚 Fix the sphinx-design example, thanks to <gh-user:recfab> (<gh-pr:738>)

## 0.19.1 - 2023-03-02

🐛 FIX `NoURI` error in doc reference resolution, for texinfo builds (<gh-pr:734>)
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -161,6 +161,7 @@
"use_issues_button": True,
"announcement": "<b>v0.19</b> is now out! See the Changelog for details",
}
html_last_updated_fmt = ""
# OpenGraph metadata
ogp_site_url = "https://myst-parser.readthedocs.io/en/latest"
# This is the image that GitHub stores for our social media previews
2 changes: 1 addition & 1 deletion docs/intro.md
Original file line number Diff line number Diff line change
@@ -133,7 +133,7 @@ For example, let's install the [sphinx-design](https://github.com/executablebook
First, install `sphinx-design`:

```shell
pip install sphinxcontrib-mermaid
pip install sphinx-design
```

Next, add it to your list of extensions in `conf.py`:
2 changes: 1 addition & 1 deletion myst_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
with bridges to [docutils](https://docutils.sourceforge.io/)
and [Sphinx](https://github.com/sphinx-doc/sphinx).
"""
__version__ = "0.19.1"
__version__ = "0.19.2"


def setup(app):
18 changes: 18 additions & 0 deletions myst_parser/config/main.py
Original file line number Diff line number Diff line change
@@ -166,6 +166,14 @@ def _test_slug_func(text: str) -> str:
return text[::-1]


def check_fence_as_directive(
inst: "MdParserConfig", field: dc.Field, value: Any
) -> None:
"""Check that the extensions are a sequence of known strings"""
deep_iterable(instance_of(str), instance_of((list, tuple, set)))(inst, field, value)
setattr(inst, field.name, set(value))


@dc.dataclass()
class MdParserConfig:
"""Configuration options for the Markdown Parser.
@@ -250,6 +258,16 @@ def __repr__(self) -> str:
},
)

fence_as_directive: Set[str] = dc.field(
default_factory=set,
metadata={
"validator": check_fence_as_directive,
"help": "Interpret a code fence as a directive, for certain language names. "
"This can be useful for fences like dot and mermaid, "
"and interoperability with other Markdown renderers.",
},
)

number_code_blocks: Sequence[str] = dc.field(
default_factory=list,
metadata={
87 changes: 61 additions & 26 deletions myst_parser/mdit_to_docutils/base.py
Original file line number Diff line number Diff line change
@@ -732,26 +732,34 @@ def render_code_block(self, token: SyntaxTreeNode) -> None:
self.current_node.append(node)

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

if (not self.md_config.commonmark_only) and (not self.md_config.gfm_only):
if language == "{eval-rst}":
if name == "{eval-rst}":
return self.render_restructuredtext(token)
if language.startswith("{") and language.endswith("}"):
return self.render_directive(token)
if name.startswith("{") and name.endswith("}"):
return self.render_directive(token, name[1:-1], arguments)
if name in self.md_config.fence_as_directive:
options = {k: str(v) for k, v in token.attrs.items()}
if "id" in options:
options["name"] = options.pop("id")
return self.render_directive(
token, name, arguments, additional_options=options
)

if not language and self.sphinx_env is not None:
if not name and self.sphinx_env is not None:
# use the current highlight setting, via the ``highlight`` directive,
# or ``highlight_language`` configuration.
language = self.sphinx_env.temp_data.get(
name = self.sphinx_env.temp_data.get(
"highlight_language", self.sphinx_env.config.highlight_language
)

lineno_start = 1
number_lines = language in self.md_config.number_code_blocks
number_lines = name in self.md_config.number_code_blocks
emphasize_lines = (
str(token.attrs.get("emphasize-lines"))
if "emphasize-lines" in token.attrs
@@ -763,8 +771,8 @@ def render_fence(self, token: SyntaxTreeNode) -> None:
number_lines = True

node = self.create_highlighted_code_block(
text,
language,
token.content,
name,
number_lines=number_lines,
lineno_start=lineno_start,
source=self.document["source"],
@@ -1525,10 +1533,11 @@ def render_myst_role(self, token: SyntaxTreeNode) -> None:
self.current_node += _nodes + messages2

def render_colon_fence(self, token: SyntaxTreeNode) -> None:
"""Render a code fence with ``:`` colon delimiters."""

info = token.info.strip() if token.info else token.info
name = info.split()[0] if info else ""
"""Render a div block, with ``:`` colon delimiters."""
# split the info into possible :::name arguments
parts = (token.info.strip() if token.info else "").split(maxsplit=1)
name = parts[0] if parts else ""
arguments = parts[1] if len(parts) > 1 else ""

if name.startswith("{") and name.endswith("}"):
if token.content.startswith(":::"):
@@ -1538,7 +1547,7 @@ def render_colon_fence(self, token: SyntaxTreeNode) -> None:
linear_token = token.token.copy()
linear_token.content = "\n" + linear_token.content
token.token = linear_token
return self.render_directive(token)
return self.render_directive(token, name[1:-1], arguments)

container = nodes.container(is_div=True)
self.add_line_and_source_path(container, token)
@@ -1661,18 +1670,37 @@ def render_restructuredtext(self, token: SyntaxTreeNode) -> None:
self.document.note_explicit_target(node, node)
self.current_node.extend(newdoc.children)

def render_directive(self, token: SyntaxTreeNode) -> None:
"""Render special fenced code blocks as directives."""
first_line = token.info.split(maxsplit=1)
name = first_line[0][1:-1]
arguments = "" if len(first_line) == 1 else first_line[1]
content = token.content
def render_directive(
self,
token: SyntaxTreeNode,
name: str,
arguments: str,
*,
additional_options: dict[str, str] | None = None,
) -> None:
"""Render special fenced code blocks as directives.
:param token: the token to render
:param name: the name of the directive
:param arguments: The remaining text on the same line as the directive name.
"""
position = token_line(token)
nodes_list = self.run_directive(name, arguments, content, position)
nodes_list = self.run_directive(
name,
arguments,
token.content,
position,
additional_options=additional_options,
)
self.current_node += nodes_list

def run_directive(
self, name: str, first_line: str, content: str, position: int
self,
name: str,
first_line: str,
content: str,
position: int,
additional_options: dict[str, str] | None = None,
) -> list[nodes.Element]:
"""Run a directive and return the generated nodes.
@@ -1681,6 +1709,8 @@ def run_directive(
May be an argument or body text, dependent on the directive
:param content: All text after the first line. Can include options.
:param position: The line number of the first line
:param additional_options: Additional options to add to the directive,
above those parsed from the content.
"""
self.document.current_line = position
@@ -1706,7 +1736,12 @@ def run_directive(
directive_class.option_spec["heading-offset"] = directives.nonnegative_int

try:
parsed = parse_directive_text(directive_class, first_line, content)
parsed = parse_directive_text(
directive_class,
first_line,
content,
additional_options=additional_options,
)
except MarkupError as error:
error = self.reporter.error(
f"Directive '{name}': {error}",
18 changes: 16 additions & 2 deletions myst_parser/parsers/directives.py
Original file line number Diff line number Diff line change
@@ -65,21 +65,28 @@ def parse_directive_text(
directive_class: type[Directive],
first_line: str,
content: str,
*,
validate_options: bool = True,
additional_options: dict[str, str] | None = None,
) -> DirectiveParsingResult:
"""Parse (and validate) the full directive text.
:param first_line: The text on the same line as the directive name.
May be an argument or body text, dependent on the directive
:param content: All text after the first line. Can include options.
:param validate_options: Whether to validate the values of options
:param additional_options: Additional options to add to the directive,
above those parsed from the content (content options take priority).
:raises MarkupError: if there is a fatal parsing/validation error
"""
parse_errors: list[str] = []
if directive_class.option_spec:
body, options, option_errors = parse_directive_options(
content, directive_class, validate=validate_options
content,
directive_class,
validate=validate_options,
additional_options=additional_options,
)
parse_errors.extend(option_errors)
body_lines = body.splitlines()
@@ -114,7 +121,10 @@ def parse_directive_text(


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

if additional_options:
# The YAML block takes priority over additional options
options = {**additional_options, **options}

# check options against spec
options_spec: dict[str, Callable] = directive_class.option_spec
unknown_options: list[str] = []
28 changes: 28 additions & 0 deletions tests/test_renderers/fixtures/myst-config.txt
Original file line number Diff line number Diff line change
@@ -468,3 +468,31 @@ My paragraph
<reference id_link="True" refid="title">
reversed
.

[fence_as_directive] --myst-fence-as-directive=unknown,admonition --myst-enable-extensions=attrs_block
.
```unknown
```

{#myname .class1}
{a=b}
```admonition title
content
```
.
<document source="<string>">
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Unknown directive type: 'unknown' [myst.directive_unknown]
<system_message level="2" line="6" source="<string>" type="WARNING">
<paragraph>
'admonition': Unknown option keys: ['a'] (allowed: ['class', 'name']) [myst.directive_parse]
<admonition classes="class1" ids="myname" names="myname">
<title>
title
<paragraph>
content

<string>:1: (WARNING/2) Unknown directive type: 'unknown' [myst.directive_unknown]
<string>:6: (WARNING/2) 'admonition': Unknown option keys: ['a'] (allowed: ['class', 'name']) [myst.directive_parse]
.
31 changes: 31 additions & 0 deletions tests/test_renderers/test_parse_directives.py
Original file line number Diff line number Diff line change
@@ -49,3 +49,34 @@ def test_parsing(file_params):
def test_parsing_errors(descript, klass, arguments, content):
with pytest.raises(MarkupError):
parse_directive_text(klass, arguments, content)


def test_additional_options():
"""Allow additional options to be passed to a directive."""
# this should be fine
result = parse_directive_text(
Note, "", "content", additional_options={"class": "bar"}
)
assert not result.warnings
assert result.options == {"class": ["bar"]}
assert result.body == ["content"]
# body on first line should also be fine
result = parse_directive_text(
Note, "content", "other", additional_options={"class": "bar"}
)
assert not result.warnings
assert result.options == {"class": ["bar"]}
assert result.body == ["content", "other"]
# additional option should not take precedence
result = parse_directive_text(
Note, "content", ":class: foo", additional_options={"class": "bar"}
)
assert not result.warnings
assert result.options == {"class": ["foo"]}
assert result.body == ["content"]
# this should warn about the unknown option
result = parse_directive_text(
Note, "", "content", additional_options={"foo": "bar"}
)
assert len(result.warnings) == 1
assert "Unknown option" in result.warnings[0]