Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/change_log/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Change Log
Python-Markdown Change Log
=========================

*under development*: version 3.5 ([Notes](release-3.5.md))

July 25, 2023: version 3.4.4 (a bug-fix release).

* Add a special case for initial `'s` to smarty extension (#1305).
Expand Down
19 changes: 19 additions & 0 deletions docs/change_log/release-3.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
title: Release Notes for v3.5

# Python-Markdown 3.5 Release Notes

Python-Markdown version 3.5 supports Python versions 3.7, 3.8, 3.9, 3.10,
3.11 and PyPy3.

## New features

The following new features have been included in the 3.5 release:

* A new configuration option has been added to the
[toc](../extensions/toc.md) extension (#1364):

* A new boolean option `nested_anchor_ids` makes it possible to generate
header anchor IDs as a concatenation of all of the parent header anchor
IDs which precede it hierarchically. This feature can be useful when
linking to specific subsections of the resultant document, as the anchor
ID will more be more specific to the header and the headers above it.
44 changes: 43 additions & 1 deletion docs/extensions/toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ The following options are provided to configure the output:

Default: `markdown.extensions.toc.slugify`

In order to use a different algorithm to define the id attributes, define and
In order to use a different algorithm to define the id attributes, define and
pass in a callable which takes the following two arguments:

* `value`: The string to slugify.
Expand All @@ -208,6 +208,48 @@ The following options are provided to configure the output:
An alternate version of the default callable supporting Unicode strings is also
provided as `markdown.extensions.toc.slugify_unicode`.

* **`nested_anchor_ids`**:
Set to `True` to set header anchor IDs to a concatenation of all of the
parent header anchor IDs which precede it hierarchically.

This feature can be useful when linking to specific subsections of the
resultant document, as the anchor ID will more be more specific to the
header and the headers above it. Unlike the default anchor ID scheme, these
more specific links are less likely to break as additions are made to the
markdown document.

Default is `False`.

For example, consider the following markdown:
```md
# Header A
## Header A
## Header B
### Header A
# Header B
## Header A
```

Without the `nested_anchor_ids` setting, the resultant HTML would be:
```html
<h1 id="header-a">Header A</h1>
<h2 id="header-a_1">Header A</h2>
<h2 id="header-b">Header B</h2>
<h3 id="header-a_2">Header A</h3>
<h1 id="header-b_1">Header B</h1>
<h2 id="header-a_3">Header A</h2>
```

With the `nested_anchor_ids` setting, the resultant HTML would be:
```html
<h1 id="header-a">Header A</h1>
<h2 id="header-a-header-a">Header A</h2>
<h2 id="header-a-header-b">Header B</h2>
<h3 id="header-a-header-b-header-a">Header A</h3>
<h1 id="header-b">Header B</h1>
<h2 id="header-b-header-a">Header A</h2>
```

* **`separator`**:
Word separator. Character which replaces white space in id. Defaults to "`-`".

Expand Down
33 changes: 31 additions & 2 deletions markdown/extensions/toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def __init__(self, md, config):
self.base_level = int(config["baselevel"]) - 1
self.slugify = config["slugify"]
self.sep = config["separator"]
self.nested_anchor_ids = parseBoolValue(config["nested_anchor_ids"], False)
self.toc_class = config["toc_class"]
self.use_anchors = parseBoolValue(config["anchorlink"])
self.anchorlink_class = config["anchorlink_class"]
Expand Down Expand Up @@ -213,6 +214,7 @@ def set_level(self, elem):
if level > 6:
level = 6
elem.tag = 'h%d' % level
return level

def add_anchor(self, c, elem_id):
anchor = etree.Element("a")
Expand Down Expand Up @@ -274,16 +276,38 @@ def run(self, doc):
if "id" in el.attrib:
used_ids.add(el.attrib["id"])

parent_anchor_ids = []
prev_anchor_id = None
prev_level = None

toc_tokens = []
for el in doc.iter():
if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
self.set_level(el)
level = self.set_level(el)
text = get_name(el)

if self.nested_anchor_ids and prev_level:
# Adjust the list of parent anchor IDs for this element
if level > prev_level:
parent_anchor_ids.append(prev_anchor_id)
elif level < prev_level:
for x in range(0, prev_level - level):
parent_anchor_ids.pop()

# Do not override pre-existing ids
if "id" not in el.attrib:
innertext = unescape(stashedHTML2text(text, self.md))
el.attrib["id"] = unique(self.slugify(innertext, self.sep), used_ids)
anchor_id = self.slugify(innertext, self.sep)

if self.nested_anchor_ids and parent_anchor_ids:
# Combine this element's slug with the ID of the parent element
anchor_id = '-'.join([parent_anchor_ids[-1], anchor_id])

el.attrib["id"] = unique(anchor_id, used_ids)
used_ids.add(el.attrib["id"])

prev_level = level
prev_anchor_id = el.attrib["id"]

if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom:
toc_tokens.append({
Expand Down Expand Up @@ -352,6 +376,11 @@ def __init__(self, **kwargs):
"Function to generate anchors based on header text - "
"Defaults to the headerid ext's slugify function."],
'separator': ['-', 'Word separator. Defaults to "-".'],
"nested_anchor_ids": [False,
'Set to `True` to set header anchor IDs to a '
'concatenation of of all of the parent header '
'anchor IDs which precede it hierarchically. '
'Defaults to False'],
"toc_depth": [6,
'Define the range of section levels to include in'
'the Table of Contents. A single integer (b) defines'
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nav:
- Test Tools: test_tools.md
- Contributing to Python-Markdown: contributing.md
- Change Log: change_log/index.md
- Release Notes for v.3.5: change_log/release-3.5.md
- Release Notes for v.3.4: change_log/release-3.4.md
- Release Notes for v.3.3: change_log/release-3.3.md
- Release Notes for v.3.2: change_log/release-3.2.md
Expand Down
93 changes: 93 additions & 0 deletions tests/test_syntax/extensions/test_toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,96 @@ def testTOCWithCustomClasses(self):
),
extensions=[TocExtension(toc_class="custom1 custom2")]
)

def testNestedAnchorIDsOff(self):
self.assertMarkdownRenders(
self.dedent(
'''
# Header A
# Header A
## Header A
## Header A
## Header B
## Header C
### Header A
# Header B
## Header A
'''
),
self.dedent(
'''
<h1 id="header-a">Header A</h1>
<h1 id="header-a_1">Header A</h1>
<h2 id="header-a_2">Header A</h2>
<h2 id="header-a_3">Header A</h2>
<h2 id="header-b">Header B</h2>
<h2 id="header-c">Header C</h2>
<h3 id="header-a_4">Header A</h3>
<h1 id="header-b_1">Header B</h1>
<h2 id="header-a_5">Header A</h2>
'''
),
extensions=[TocExtension(nested_anchor_ids=False)]
)

def testNestedAnchorIDsOn(self):
self.assertMarkdownRenders(
self.dedent(
'''
# Header A
# Header A
## Header A
## Header A
## Header B
## Header C
### Header A
# Header B
## Header A
'''
),
self.dedent(
'''
<h1 id="header-a">Header A</h1>
<h1 id="header-a_1">Header A</h1>
<h2 id="header-a_1-header-a">Header A</h2>
<h2 id="header-a_1-header-a_1">Header A</h2>
<h2 id="header-a_1-header-b">Header B</h2>
<h2 id="header-a_1-header-c">Header C</h2>
<h3 id="header-a_1-header-c-header-a">Header A</h3>
<h1 id="header-b">Header B</h1>
<h2 id="header-b-header-a">Header A</h2>
'''
),
extensions=[TocExtension(nested_anchor_ids=True)]
)

def testNestedAnchorIDsOnWithBaseLevel(self):
self.assertMarkdownRenders(
self.dedent(
'''
# Header A
# Header A
## Header A
## Header A
## Header B
## Header C
### Header A
# Header B
## Header A
'''
),
self.dedent(
'''
<h3 id="header-a">Header A</h3>
<h3 id="header-a_1">Header A</h3>
<h4 id="header-a_1-header-a">Header A</h4>
<h4 id="header-a_1-header-a_1">Header A</h4>
<h4 id="header-a_1-header-b">Header B</h4>
<h4 id="header-a_1-header-c">Header C</h4>
<h5 id="header-a_1-header-c-header-a">Header A</h5>
<h3 id="header-b">Header B</h3>
<h4 id="header-b-header-a">Header A</h4>
'''
),
extensions=[TocExtension(nested_anchor_ids=True, baselevel=3)]
)