Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion myst_parser/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ def __repr__(self) -> str:
default=False,
metadata={
"validator": instance_of(bool),
"help": "Open all external links in a new tab",
"help": "Open all external links in a new tab "
'(sets target="_blank" and rel="noreferrer noopener", '
"unless the link already sets them)",
},
)

Expand Down
24 changes: 18 additions & 6 deletions myst_parser/mdit_to_docutils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ def copy_attributes(
value = converters[key](str(value))
except ValueError:
self.create_warning(
f"Invalid {key!r} attribute value: {token.attrs[key]!r}",
f"Invalid {key!r} attribute value: {value!r}",
MystWarnings.INVALID_ATTRIBUTE,
line=token_line(token, default=0),
append_to=node,
Expand Down Expand Up @@ -659,7 +659,7 @@ def _parse_linenos(emphasize_lines: str, num_lines: int) -> list[int]:

hl_lines = parselinenos(emphasize_lines, num_lines)
if any(i >= num_lines for i in hl_lines):
raise ValueError(f"out of range(1-{num_lines}")
raise ValueError(f"out of range(1-{num_lines})")

return [x + 1 for x in hl_lines if x < num_lines]

Expand Down Expand Up @@ -851,8 +851,20 @@ def generate_heading_target(
# TODO this is purely to mimic docutils, but maybe we don't need it?
# (since we have the slugify logic below)
name = nodes.fully_normalize_name(implicit_text)
node["names"].append(name)
self.document.note_implicit_target(node, node)
# Register only this new, implicit name.
# ``note_implicit_target`` re-registers *every* name already on the node,
# as an implicit one -- including an explicit ``{#id}`` name that
# ``copy_attributes`` has already added and registered. The node then
# collides with itself: docutils demotes the explicit name into
# ``dupnames`` while its name map still points here, so a later,
# genuine duplicate of that name raises
# ``ValueError: list.remove(x): x not in list``.
explicit_names = node["names"]
node["names"] = [name]
try:
self.document.note_implicit_target(node, node)
finally:
node["names"] = explicit_names + node["names"]

if level > self.md_config.heading_anchors:
return
Expand Down Expand Up @@ -997,8 +1009,8 @@ def render_link_url(
self.add_line_and_source_path(ref_node, token)
attribute_keys = ["class", "id", "reftitle", "target", "rel"]
if self.md_config.links_external_new_tab:
token.attrs["target"] = "_blank"
token.attrs["rel"] = "noreferer noopener"
token.attrs.setdefault("target", "_blank")
token.attrs.setdefault("rel", "noreferrer noopener")
self.copy_attributes(
token, ref_node, attribute_keys, aliases={"title": "reftitle"}
)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_docutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ def test_help_text():
assert not exc.code

assert "MyST options" in stream.getvalue()
# the option's help text is its only user-facing documentation
assert 'rel="noreferrer noopener"' in " ".join(stream.getvalue().split())


def test_include_from_rst(tmp_path):
Expand Down
24 changes: 24 additions & 0 deletions tests/test_renderers/fixtures/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,27 @@ list-style
<paragraph>
b
.

heading with id
.
{#hid}
# First
.
<document source="<src>/index.md">
<section ids="hid" names="hid first">
<title>
First
.

rubric with id
.
```{note}
{#rid}
## First
```
.
<document source="<src>/index.md">
<note>
<rubric ids="rid" level="2" names="rid first">
First
.
67 changes: 66 additions & 1 deletion tests/test_renderers/fixtures/myst-config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,46 @@ a
<string>:1: (WARNING/2) Invalid 'align' attribute value: 'other' [myst.attribute]
.

[attrs_inline_image_warnings_aliases] --myst-enable-extensions=attrs_inline
.
![a](b){w=1x h=2x a=other }
.
<document source="<string>">
<paragraph>
<image alt="a" uri="b">
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid 'width' attribute value: '1x' [myst.attribute]
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid 'height' attribute value: '2x' [myst.attribute]
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid 'align' attribute value: 'other' [myst.attribute]

<string>:1: (WARNING/2) Invalid 'width' attribute value: '1x' [myst.attribute]
<string>:1: (WARNING/2) Invalid 'height' attribute value: '2x' [myst.attribute]
<string>:1: (WARNING/2) Invalid 'align' attribute value: 'other' [myst.attribute]
.

[attrs_inline_image_warnings_alias_and_long] --myst-enable-extensions=attrs_inline
.
![a](b){w=1x width=2x}
.
<document source="<string>">
<paragraph>
<image alt="a" uri="b">
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid 'width' attribute value: '1x' [myst.attribute]
<system_message level="2" line="1" source="<string>" type="WARNING">
<paragraph>
Invalid 'width' attribute value: '2x' [myst.attribute]

<string>:1: (WARNING/2) Invalid 'width' attribute value: '1x' [myst.attribute]
<string>:1: (WARNING/2) Invalid 'width' attribute value: '2x' [myst.attribute]
.

[attrs_block] --myst-enable-extensions=attrs_block
.
{#myid1 .class1 .class2}
Expand Down Expand Up @@ -529,6 +569,31 @@ content
.
<document source="<string>">
<paragraph>
<reference refuri="https://example.com" rel="noreferer noopener" target="_blank">
<reference refuri="https://example.com" rel="noreferrer noopener" target="_blank">
text
.

[links-external-new-tab-authored] --myst-links-external-new-tab="true" --myst-enable-extensions=attrs_inline
.
[a](https://example.com){target=_self rel=nofollow}

[a](https://example.com){target=_self}

[a](https://example.com){rel=nofollow}

<https://example.com>{rel=nofollow}
.
<document source="<string>">
<paragraph>
<reference refuri="https://example.com" rel="nofollow" target="_self">
a
<paragraph>
<reference refuri="https://example.com" rel="noreferrer noopener" target="_self">
a
<paragraph>
<reference refuri="https://example.com" rel="nofollow" target="_blank">
a
<paragraph>
<reference refuri="https://example.com" rel="nofollow" target="_blank">
https://example.com
.
24 changes: 24 additions & 0 deletions tests/test_renderers/test_parse_linenos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Test ``DocutilsRenderer._parse_linenos``.

This is the helper behind the ``emphasize-lines`` attribute,
and is only reached on the sphinx code path.
"""

import re

import pytest

pytest.importorskip("sphinx")

from myst_parser.mdit_to_docutils.base import DocutilsRenderer


def test_parse_linenos():
"""A line within the block is returned, 1-based."""
assert DocutilsRenderer._parse_linenos("2", 3) == [2]


def test_parse_linenos_out_of_range():
"""A line past the end of the block reports the allowed range."""
with pytest.raises(ValueError, match=re.escape("out of range(1-3)")):
DocutilsRenderer._parse_linenos("5", 3)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
extensions = ["myst_parser"]
exclude_patterns = ["_build"]
myst_enable_extensions = ["attrs_block"]
21 changes: 21 additions & 0 deletions tests/test_sphinx/sourcedirs/attrs_block_duplicate_ids/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Page

{#sec}
## First

{#sec}
## Second

{#para}
Paragraph one.

{#para}
Paragraph two.

```{note}
{#rub}
### Third

{#rub}
### Fourth
```
27 changes: 27 additions & 0 deletions tests/test_sphinx/test_sphinx_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,33 @@ def test_substitutions_missing(
)


@pytest.mark.sphinx(
buildername="html",
srcdir=os.path.join(SOURCE_DIR, "attrs_block_duplicate_ids"),
freshenv=True,
)
def test_attrs_block_duplicate_ids(
app,
status,
warning,
):
"""Test that a duplicated ``{#id}`` warns, rather than aborting the build."""
app.build()
assert "build succeeded" in status.getvalue() # Build succeeded
warnings = strip_colors(warning.getvalue()).strip().splitlines()
assert len(warnings) == 3
assert warnings[0].endswith(
'index.md:7: WARNING: Duplicate explicit target name: "sec". [docutils]'
)
assert warnings[1].endswith(
'index.md:: WARNING: Duplicate explicit target name: "para". [docutils]'
)
assert warnings[2].endswith(
'index.md:20: WARNING: Duplicate explicit target name: "rub". [docutils]'
)
assert Path(app.outdir, "index.html").exists()


@pytest.mark.sphinx(
buildername="gettext", srcdir=os.path.join(SOURCE_DIR, "gettext"), freshenv=True
)
Expand Down
Loading