Skip to content

Commit

Permalink
馃憣 IMPROVE: Add myst_heading_slug_func option (#359)
Browse files Browse the repository at this point in the history
For specifying a custom function to auto-generate heading anchors
  • Loading branch information
jpmckinney committed May 6, 2021
1 parent 15ff74e commit a39d236
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/using/intro.md
Expand Up @@ -246,6 +246,9 @@ To do so, use the keywords beginning `myst_`.
* - `myst_heading_anchors`
- `None`
- Enable auto-generated heading anchors, up to a maximum level, [see here](syntax/header-anchors) for details.
* - `myst_heading_slug_func`
- `None`
- Use the specified function to auto-generate heading anchors, [see here](syntax/header-anchors) for details.
* - `myst_substitutions`
- `{}`
- A mapping of keys to substitutions, used globally for all MyST documents when the "substitution" extension is enabled.
Expand Down
1 change: 1 addition & 0 deletions docs/using/syntax-optional.md
Expand Up @@ -312,6 +312,7 @@ To achieve this, section headings must be assigned anchors, which can be achieve
by setting `myst_heading_anchors = 2` in your `conf.py`.
This configures heading anchors to be assigned to both `h1` and `h2` level headings.
The anchor "slugs" created aim to follow the [GitHub implementation](https://github.com/Flet/github-slugger); lower-case text, removing punctuation, replacing spaces with `-`, uniqueness *via* suffix enumeration `-1`.
To change the slug function, set `myst_heading_slug_func` in your `conf.py` to a function that accepts a string and returns a string.
You can inspect the links that will be created using the command-line tool:

```console
Expand Down
21 changes: 18 additions & 3 deletions myst_parser/main.py
@@ -1,7 +1,14 @@
from typing import Any, Dict, Iterable, Optional, Tuple, Type, Union
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Type, Union

import attr
from attr.validators import deep_iterable, deep_mapping, in_, instance_of, optional
from attr.validators import (
deep_iterable,
deep_mapping,
in_,
instance_of,
is_callable,
optional,
)
from markdown_it import MarkdownIt
from markdown_it.renderer import RendererHTML
from mdit_py_plugins.amsmath import amsmath_plugin
Expand Down Expand Up @@ -77,6 +84,10 @@ def check_extensions(self, attribute, value):
default=None, validator=optional(in_([1, 2, 3, 4, 5, 6, 7]))
)

heading_slug_func: Optional[Callable[[str], str]] = attr.ib(
default=None, validator=optional(is_callable())
)

html_meta: Dict[str, str] = attr.ib(
factory=dict,
validator=deep_mapping(instance_of(str), instance_of(str), instance_of(dict)),
Expand Down Expand Up @@ -178,7 +189,11 @@ def default_parser(config: MdParserConfig) -> MarkdownIt:
if "substitution" in config.enable_extensions:
md.use(substitution_plugin, *config.sub_delimiters)
if config.heading_anchors is not None:
md.use(anchors_plugin, max_level=config.heading_anchors)
md.use(
anchors_plugin,
max_level=config.heading_anchors,
slug_func=config.heading_slug_func,
)
for name in config.disable_syntax:
md.disable(name, True)

Expand Down
6 changes: 6 additions & 0 deletions tests/test_sphinx/sourcedirs/heading_slug_func/conf.py
@@ -0,0 +1,6 @@
from docutils.nodes import make_id

extensions = ["myst_parser"]
exclude_patterns = ["_build"]
myst_heading_anchors = 2
myst_heading_slug_func = make_id
3 changes: 3 additions & 0 deletions tests/test_sphinx/sourcedirs/heading_slug_func/index.md
@@ -0,0 +1,3 @@
# Hyphen - 1

## Dot 1.1
27 changes: 27 additions & 0 deletions tests/test_sphinx/test_sphinx_builds.py
Expand Up @@ -122,6 +122,33 @@ def test_references_singlehtml(
)


@pytest.mark.sphinx(
buildername="html",
srcdir=os.path.join(SOURCE_DIR, "heading_slug_func"),
freshenv=True,
)
def test_heading_slug_func(
app,
status,
warning,
get_sphinx_app_doctree,
get_sphinx_app_output,
remove_sphinx_builds,
):
"""Test heading_slug_func configuration."""
app.build()

assert "build succeeded" in status.getvalue() # Build succeeded
warnings = warning.getvalue().strip()
assert warnings == ""

try:
get_sphinx_app_doctree(app, docname="index", regress=True)
finally:
get_sphinx_app_doctree(app, docname="index", resolve=True, regress=True)
get_sphinx_app_output(app, filename="index.html", regress_html=True)


@pytest.mark.sphinx(
buildername="html",
srcdir=os.path.join(SOURCE_DIR, "extended_syntaxes"),
Expand Down
22 changes: 22 additions & 0 deletions tests/test_sphinx/test_sphinx_builds/test_heading_slug_func.html
@@ -0,0 +1,22 @@
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<section id="hyphen-1">
<h1>
Hyphen - 1
<a class="headerlink" href="#hyphen-1" title="Permalink to this headline">
</a>
</h1>
<section id="dot-1-1">
<h2>
Dot 1.1
<a class="headerlink" href="#dot-1-1" title="Permalink to this headline">
</a>
</h2>
</section>
</section>
</div>
</div>
</div>
@@ -0,0 +1,7 @@
<document source="index.md">
<section ids="hyphen-1" myst-anchor="index.md#hyphen-1" names="hyphen\ -\ 1">
<title>
Hyphen - 1
<section ids="dot-1-1" myst-anchor="index.md#dot-1-1" names="dot\ 1.1">
<title>
Dot 1.1
@@ -0,0 +1,7 @@
<document source="index.md">
<section ids="hyphen-1" myst-anchor="index.md#hyphen-1" names="hyphen\ -\ 1">
<title>
Hyphen - 1
<section ids="dot-1-1" myst-anchor="index.md#dot-1-1" names="dot\ 1.1">
<title>
Dot 1.1

0 comments on commit a39d236

Please sign in to comment.