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
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
("py:exc", "MarkupError"),
("py:class", "sphinx.util.typing.Inventory"),
("py:class", "sphinx.writers.html.HTMLTranslator"),
("py:class", "sphinx.writers.latex.LaTeXTranslator"),
("py:obj", "sphinx.transforms.post_transforms.ReferencesResolver"),
]

Expand Down
14 changes: 14 additions & 0 deletions docs/syntax/math.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,17 @@ a_{21}& =b_{21}&
\end{align}

:::

A numbered environment can contain a single `\label`, to reference it with the `eq` role:

:::{syntax-example}
:highlight: latex

\begin{equation}
e^{i\pi} + 1 = 0 \label{eq:euler}
\end{equation}

The equation {eq}`eq:euler` is Euler's identity.
:::

See [](#syntax/amsmath) for more details.
18 changes: 14 additions & 4 deletions docs/syntax/optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,20 @@ a_{21}& =b_{21}&

:::

:::{note}
`\labels` inside the environment are not currently identified, and so cannot be referenced.
We hope to implement this in a future update (see [executablebooks/MyST-Parser#202](https://github.com/executablebooks/MyST-Parser/issues/202))!
:::
A numbered environment can be given a label with `\label{name}`,
which can then be referenced with the `eq` role, e.g. `` {eq}`name` ``:

```latex
\begin{equation}
e^{i\pi} + 1 = 0 \label{eq:euler}
\end{equation}
```

The whole environment is given a single equation number,
so only one `\label` is supported per environment.
If an environment contains more than one `\label`, a `myst.math_label` warning is emitted and none of them are used
(see [executablebooks/MyST-Parser#202](https://github.com/executablebooks/MyST-Parser/issues/202)).
Labels in unnumbered (`*`) environments, or in LaTeX `%` comments, are ignored.

This syntax will also work when nested in other block elements, like lists or quotes:

Expand Down
32 changes: 32 additions & 0 deletions myst_parser/mdit_to_docutils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ def make_document(source_path="notset", parser_cls=RSTParser) -> nodes.document:
r"{{\s*(uri|scheme|netloc|path|params|query|fragment)\s*}}"
)
REGEX_DIRECTIVE_START = re.compile(r"^[\s]{0,3}([`]{3,10}|[~]{3,10}|[:]{3,10})\{")
# a ``%`` comment, unless the ``%`` is escaped (but ``\\%`` is a line break + comment)
REGEX_LATEX_COMMENT = re.compile(r"(?<!\\)(?:\\\\)*%.*$", re.MULTILINE)
REGEX_LATEX_LABEL = re.compile(r"\\label\s*\{([^{}]*)\}")


def token_line(token: SyntaxTreeNode, default: int | None = None) -> int:
Expand Down Expand Up @@ -1521,6 +1524,32 @@ def render_math_block_label(self, token: SyntaxTreeNode) -> None:
self.document.note_explicit_target(node, node)
self.current_node.append(node)

def get_amsmath_label(self, token: SyntaxTreeNode) -> str | None:
"""Return the ``\\label{...}`` of a numbered amsmath environment.

Only a single label per environment is supported,
since the whole environment is given a single equation number.
If there are multiple labels, a warning is emitted and none are used.
"""
if token.meta["numbered"] == "*":
return None
content = REGEX_LATEX_COMMENT.sub("", token.content)
labels = [
label.strip()
for label in REGEX_LATEX_LABEL.findall(content)
if label.strip()
]
if len(labels) > 1:
self.create_warning(
"Only one \\label is supported per amsmath environment, "
f"ignoring: {', '.join(labels)}",
MystWarnings.MATH_LABEL,
line=token_line(token, 0),
append_to=self.current_node,
)
return None
return labels[0] if labels else None

def render_amsmath(self, token: SyntaxTreeNode) -> None:
# note docutils does not currently support the nowrap attribute
# or equation numbering, so this is overridden in the sphinx renderer
Expand All @@ -1530,6 +1559,9 @@ def render_amsmath(self, token: SyntaxTreeNode) -> None:
if token.meta["numbered"] != "*":
node["numbered"] = True
self.add_line_and_source_path(node, token)
if label := self.get_amsmath_label(token):
node["names"].append(nodes.fully_normalize_name(label))
self.document.note_explicit_target(node, node)
self.current_node.append(node)

def render_footnote_ref(self, token: SyntaxTreeNode) -> None:
Expand Down
11 changes: 6 additions & 5 deletions myst_parser/mdit_to_docutils/sphinx_.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,9 @@ def render_amsmath(self, token: SyntaxTreeNode) -> None:
content = token.content

if token.meta["numbered"] != "*":
# TODO how to parse and reference labels within environment?
# for now we give create a unique hash, so the equation will be numbered
# but there will be no reference clashes
label = self._random_label()
# use the environment's \label, if it has one, so that it can be referenced,
# otherwise create a unique hash, so the equation will still be numbered
label = self.get_amsmath_label(token) or self._random_label()
node = nodes.math_block(
content,
content,
Expand All @@ -213,14 +212,16 @@ def render_amsmath(self, token: SyntaxTreeNode) -> None:
classes=["amsmath"],
label=label,
)
# set the line first, so that duplicate label warnings have a location
self.add_line_and_source_path(node, token)
target = self.add_math_target(node)
self.add_line_and_source_path(target, token)
self.current_node.append(target)
else:
node = nodes.math_block(
content, content, nowrap=True, number=None, classes=["amsmath"]
)
self.add_line_and_source_path(node, token)
self.add_line_and_source_path(node, token)
self.current_node.append(node)

def add_math_target(self, node: nodes.math_block) -> nodes.target:
Expand Down
28 changes: 27 additions & 1 deletion myst_parser/sphinx_ext/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""The setup for the sphinx extension."""

from typing import Any
import re
from typing import TYPE_CHECKING, Any

from docutils import nodes
from sphinx.application import Sphinx
Expand All @@ -16,6 +17,9 @@
from myst_parser.parsers.mdit import linkify_available
from myst_parser.warnings_ import MystWarnings

if TYPE_CHECKING:
from sphinx.writers.latex import LaTeXTranslator


def setup_sphinx(app: Sphinx, load_parser: bool = False) -> None:
"""Initialize all settings and transforms in Sphinx.
Expand Down Expand Up @@ -58,6 +62,10 @@ def setup_sphinx(app: Sphinx, load_parser: bool = False) -> None:
html=(visit_container_html, depart_container_html),
)

# override the latex writer visit method for math_block,
# so that the label of an amsmath environment is placed within it
app.add_node(nodes.math_block, override=True, latex=(visit_math_block_latex, None))

for name, default, field in MdParserConfig().as_triple():
if "sphinx" not in field.metadata.get("omit", []):
# TODO add types?
Expand All @@ -67,6 +75,24 @@ def setup_sphinx(app: Sphinx, load_parser: bool = False) -> None:
app.connect("builder-inited", override_mathjax)


def visit_math_block_latex(self: "LaTeXTranslator", node: nodes.math_block) -> None:
"""Write amsmath blocks, that use their own ``\\label``, to LaTeX.

For ``nowrap`` math, sphinx writes the label before the content,
where it would not refer to the equation number of the amsmath environment.
So instead we rename the ``\\label`` within the environment.
"""
label = node.get("label")
if "amsmath" in node["classes"] and label:
regex = re.compile(r"\\label\s*\{\s*" + re.escape(label) + r"\s*\}")
content = node.astext()
if regex.search(content):
new_label = f"\\label{{equation:{node['docname']}:{label}}}"
self.body.append(regex.sub(lambda _: new_label, content))
raise nodes.SkipNode
type(self).visit_math_block(self, node)


def create_myst_config(app):
"""Create the myst config object and add it to the sphinx environment."""
from sphinx.util import logging
Expand Down
2 changes: 2 additions & 0 deletions myst_parser/warnings_.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class MystWarnings(Enum):
"""Invalid attribute value."""
SUBSTITUTION = "substitution"
"""Substitution could not be resolved."""
MATH_LABEL = "math_label"
"""A label in LaTeX math could not be used."""


def _is_suppressed_warning(
Expand Down
100 changes: 100 additions & 0 deletions tests/test_renderers/fixtures/amsmath.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,103 @@ In list:
a = 1
\end{equation}
.

With label:
.
\begin{equation}
a = 1 \label{eq:my-label}
\end{equation}
.
<document source="<src>/index.md">
<target ids="equation-eq-my-label">
<math_block classes="amsmath" docname="index" label="eq:my-label" nowrap="True" number="1" xml:space="preserve">
\begin{equation}
a = 1 \label{eq:my-label}
\end{equation}
.

With label and reference:
.
\begin{align}
\label{eq1}
a &= 1 \\
b &= 2
\end{align}

{eq}`eq1`
.
<document source="<src>/index.md">
<target ids="equation-eq1">
<math_block classes="amsmath" docname="index" label="eq1" nowrap="True" number="1" xml:space="preserve">
\begin{align}
\label{eq1}
a &= 1 \\
b &= 2
\end{align}
<paragraph>
<pending_xref refdoc="index" refdomain="math" refexplicit="False" reftarget="eq1" reftype="eq" refwarn="True">
<literal classes="xref eq">
eq1
.

Label in comment is ignored:
.
\begin{equation}
a = 1 % \label{eq1}
\end{equation}
.
<document source="<src>/index.md">
<target ids="equation-mock-uuid">
<math_block classes="amsmath" docname="index" label="mock-uuid" nowrap="True" number="1" xml:space="preserve">
\begin{equation}
a = 1 % \label{eq1}
\end{equation}
.

Label after escaped percent:
.
\begin{equation}
a = 1\% \label{eq1}
\end{equation}
.
<document source="<src>/index.md">
<target ids="equation-eq1">
<math_block classes="amsmath" docname="index" label="eq1" nowrap="True" number="1" xml:space="preserve">
\begin{equation}
a = 1\% \label{eq1}
\end{equation}
.

Label in no number environment is ignored:
.
\begin{equation*}
a = 1 \label{eq1}
\end{equation*}
.
<document source="<src>/index.md">
<math_block classes="amsmath" nowrap="True" number="True" xml:space="preserve">
\begin{equation*}
a = 1 \label{eq1}
\end{equation*}
.

Multiple labels:
.
\begin{align}
a &= 1 \label{eq1} \\
b &= 2 \label{eq2}
\end{align}
.
<document source="<src>/index.md">
<system_message level="2" line="1" source="<src>/index.md" type="WARNING">
<paragraph>
Only one \label is supported per amsmath environment, ignoring: eq1, eq2 [myst.math_label]
<target ids="equation-mock-uuid">
<math_block classes="amsmath" docname="index" label="mock-uuid" nowrap="True" number="1" xml:space="preserve">
\begin{align}
a &= 1 \label{eq1} \\
b &= 2 \label{eq2}
\end{align}

<src>/index.md:1: WARNING: Only one \label is supported per amsmath environment, ignoring: eq1, eq2 [myst.math_label]
.
31 changes: 31 additions & 0 deletions tests/test_renderers/fixtures/docutil_syntax_extensions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ a
\end{equation*}
.

[amsmath-label] --myst-enable-extensions=amsmath
.
\begin{equation}
a = 1 \label{eq:a}
\end{equation}

\begin{align}
a &= 1 \label{eq1} \\
b &= 2 \label{eq2}
\end{align}

[link](#eq:a)
.
<document source="<string>">
<math_block classes="amsmath" ids="eq-a" names="eq:a" nowrap="True" numbered="True" xml:space="preserve">
\begin{equation}
a = 1 \label{eq:a}
\end{equation}
<system_message level="2" line="5" source="<string>" type="WARNING">
<paragraph>
Only one \label is supported per amsmath environment, ignoring: eq1, eq2 [myst.math_label]
<math_block classes="amsmath" nowrap="True" numbered="True" xml:space="preserve">
\begin{align}
a &= 1 \label{eq1} \\
b &= 2 \label{eq2}
\end{align}
<paragraph>
<reference id_link="True" refid="eq-a">
link
.

[deflist] --myst-enable-extensions=deflist
.
term
Expand Down
14 changes: 10 additions & 4 deletions tests/test_renderers/test_fixtures_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from docutils.core import Publisher
from pytest_param_files import ParamTestData
from sphinx.transforms import SphinxTransformer
from sphinx.util.console import strip_colors
from sphinx_pytest.plugin import CreateDoctree

from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer
Expand Down Expand Up @@ -179,12 +180,17 @@ def test_amsmath(
):
monkeypatch.setattr(SphinxRenderer, "_random_label", lambda self: "mock-uuid")
sphinx_doctree_no_tr.set_conf(
{"extensions": ["myst_parser"], "myst_enable_extensions": ["amsmath"]}
{
"extensions": ["myst_parser"],
"myst_enable_extensions": ["amsmath"],
"show_warning_types": True,
}
)
result = sphinx_doctree_no_tr(file_params.content, "index.md")
file_params.assert_expected(
normalize_doctree_xml(result.pformat("index")), rstrip_lines=True
)
outcome = normalize_doctree_xml(result.pformat("index"))
if warnings := strip_colors(result.warnings).strip():
outcome += "\n\n" + warnings
file_params.assert_expected(outcome, rstrip_lines=True)


@pytest.mark.param_file(FIXTURE_PATH / "containers.md")
Expand Down
6 changes: 6 additions & 0 deletions tests/test_sphinx/sourcedirs/extended_syntaxes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ a $$c=3$$ b
b=2
\end{equation}

\begin{equation}
c=4 \label{eq:labelled}
\end{equation}

Reference to {eq}`eq:labelled`

```{math}
c=3

Expand Down
Loading