Skip to content

Commit

Permalink
Combine parent header and content tab slug via new option (#2105)
Browse files Browse the repository at this point in the history
* Combine parent header and content tab slug via new option

* Add tests and documentation
  • Loading branch information
facelessuser authored Jul 10, 2023
1 parent 58098ef commit 5620778
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 13 deletions.
6 changes: 6 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 10.1

- **NEW**: Add new `combine_header_slug` option in legacy Tabbed extension and new Block Tab extension that will
prefix a content tab's slug with the parent header's slug. This allows for content tab slugs that are scoped to the
header they are under.

## 10.0.1

- **FIX**: Regression related to snippets nested deeply under specified base path.
Expand Down
30 changes: 25 additions & 5 deletions docs/src/markdown/extensions/blocks/plugins/tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ If `slugify` is given a slug function (you can use any that [ship with Pymdownx
the Tabbed extension will generate IDs from the tab titles just like headers. `separator` allows for the specifying of
the word separator (`-` is the default).

If you'd like the slugs to be prefixed with the slug of the parent header, you can enable the `combine_header_slug`
option. If you had the following example, normally the header slug would be `header` and the content tab would have the
slug `tab`.

```
# header
/// tab | tab
content
///
```

With `combine_header_slug` enabled, the header slug would still be `header`, but now the content tab slug would be
`header-tab`.

/// new | New 10.1
`combine_header_slug` is new in 10.1
///

## Additional Topics

As Tab shares the same output and functionality as the [Tabbed extension](../../tabbed.md), you can check out the
Expand All @@ -127,11 +146,12 @@ documentation there to learn the following:

## Global Options

Options | Type | Descriptions
----------------- | -------- | ------------
`alternate_style` | bool | Use the experimental, alternative style.
`slugify` | function | A function to generate slugs from tab titles.
`separator` | string | Default word separator when generating slugs.
Options | Type | Descriptions
--------------------- | -------- | ------------
`alternate_style` | bool | Use the experimental, alternative style.
`slugify` | function | A function to generate slugs from tab titles.
`separator` | string | Default word separator when generating slugs.
`combine_header_slug` | bool | Combine the parent header slug with the tab content slug.

## Per Block Options

Expand Down
29 changes: 24 additions & 5 deletions docs/src/markdown/extensions/tabbed.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ If `slugify` is given a slug function (you can use any that [ship with Pymdownx
Tabbed extension will generate IDs from the tab titles just like headers. `separator` allows for the specifying of the
word separator (`-` is the default).

If you'd like the slugs to be prefixed with the slug of the parent header, you can enable the `combine_header_slug`
option. If you had the following example, normally the header slug would be `header` and the content tab would have the
slug `tab`.

```
# header
=== "tab"
content
```

With `combine_header_slug` enabled, the header slug would still be `header`, but now the content tab slug would be
`header-tab`.

/// new | New 10.1
`combine_header_slug` is new in 10.1
///

## Styling with CSS

In order to use tabbed blocks, some additional CSS is needed. You can check out the configuration below which will
Expand Down Expand Up @@ -598,8 +616,9 @@ are overflowed tabs, and scrolls tabs into view smoothly.

## Options

Option | Type | Default | Description
----------------- | -------- | ------------- | -----------
`alternate_style` | bool | `#!py3 False` | Use the experimental, alternative style.
`slugify` | function | `#!py3 None` | A function to generate slugs from tab titles.
`separator` | string | `#!py3 '-'` | Default word separator when generating slugs.
Option | Type | Default | Description
--------------------- | -------- | ------------- | -----------
`alternate_style` | bool | `#!py3 False` | Use the experimental, alternative style.
`slugify` | function | `#!py3 None` | A function to generate slugs from tab titles.
`separator` | string | `#!py3 '-'` | Default word separator when generating slugs.
`combine_header_slug` | bool | Combine the parent header slug with the tab content slug.
2 changes: 2 additions & 0 deletions docs/src/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ markdown_extensions:
- pymdownx.blocks.definition:
- pymdownx.blocks.tab:
alternate_style: True
combine_header_slug: True
slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}}
- tools.collapse_code:
expand_text: ''
collapse_text: ''
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ markdown_extensions:
- pymdownx.blocks.definition:
- pymdownx.blocks.tab:
alternate_style: True
combine_header_slug: True
slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}}
- tools.collapse_code:
expand_text: ''
collapse_text: ''
Expand Down
2 changes: 1 addition & 1 deletion pymdownx/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(10, 0, 1, "final")
__version_info__ = Version(10, 1, 0, "final")
__version__ = __version_info__._get_canonical()
45 changes: 44 additions & 1 deletion pymdownx/blocks/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .block import Block, type_boolean
from ..blocks import BlocksExtension

HEADERS = {'h1', 'h2', 'h3', 'h4', 'h5', 'h6'}


class TabbedTreeprocessor(Treeprocessor):
"""Tab tree processor."""
Expand All @@ -16,15 +18,48 @@ def __init__(self, md, config):

self.alternate = config['alternate_style']
self.slugify = config['slugify']
self.combine_header_slug = config['combine_header_slug']
self.sep = config["separator"]

def get_parent_header_slug(self, root, header_map, parent_map, el):
"""Attempt retrieval of parent header slug."""

parent = el
last_parent = parent
while parent is not root:
last_parent = parent
parent = parent_map[parent]
if parent in header_map:
headers = header_map[parent]
header = None
for i in list(parent):
if i is el and header is None:
break
if i is last_parent:
return header.attrib.get("id", '')
if i in headers:
header = i
return ''

def run(self, doc):
"""Update tab IDs."""

# Get a list of id attributes
used_ids = set()
parent_map = {}
header_map = {}

if self.combine_header_slug:
parent_map = dict((c, p) for p in doc.iter() for c in p)

for el in doc.iter():
if "id" in el.attrib:
if self.combine_header_slug and el.tag in HEADERS:
parent = parent_map[el]
if parent in header_map:
header_map[parent].append(el)
else:
header_map[parent] = [el]
used_ids.add(el.attrib["id"])

for el in doc.iter():
Expand All @@ -50,7 +85,14 @@ def run(self, doc):
for inpt, label in zip(inputs, labels):
text = toc.get_name(label)
innertext = toc.unescape(toc.stashedHTML2text(text, self.md))
slug = toc.unique(self.slugify(innertext, self.sep), used_ids)
if self.combine_header_slug:
parent_slug = self.get_parent_header_slug(doc, header_map, parent_map, el)
else:
parent_slug = ''
slug = self.slugify(innertext, self.sep)
if parent_slug:
slug = parent_slug + self.sep + slug
slug = toc.unique(slug, used_ids)
inpt.attrib["id"] = slug
label.attrib["for"] = slug

Expand Down Expand Up @@ -245,6 +287,7 @@ def __init__(self, *args, **kwargs):
self.config = {
'alternate_style': [False, "Use alternate style - Default: False"],
'slugify': [0, "Slugify function used to create tab specific IDs - Default: None"],
'combine_header_slug': [False, "Combine the tab slug with the slug of the parent header - Default: False"],
'separator': ['-', "Slug separator - Default: '-'"]
}

Expand Down
45 changes: 44 additions & 1 deletion pymdownx/tabbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import xml.etree.ElementTree as etree
import re

HEADERS = {'h1', 'h2', 'h3', 'h4', 'h5', 'h6'}


class TabbedProcessor(BlockProcessor):
"""Tabbed block processor."""
Expand Down Expand Up @@ -307,14 +309,47 @@ def __init__(self, md, config):
self.slugify = config["slugify"]
self.alternate = config["alternate_style"]
self.sep = config["separator"]
self.combine_header_slug = config["combine_header_slug"]

def get_parent_header_slug(self, root, header_map, parent_map, el):
"""Attempt retrieval of parent header slug."""

parent = el
last_parent = parent
while parent is not root:
last_parent = parent
parent = parent_map[parent]
if parent in header_map:
headers = header_map[parent]
header = None
for i in list(parent):
if i is el and header is None:
break
if i is last_parent:
return header.attrib.get("id", '')
if i in headers:
header = i
return ''

def run(self, doc):
"""Update tab IDs."""

# Get a list of id attributes
used_ids = set()
parent_map = {}
header_map = {}

if self.combine_header_slug:
parent_map = dict((c, p) for p in doc.iter() for c in p)

for el in doc.iter():
if "id" in el.attrib:
if self.combine_header_slug and el.tag in HEADERS:
parent = parent_map[el]
if parent in header_map:
header_map[parent].append(el)
else:
header_map[parent] = [el]
used_ids.add(el.attrib["id"])

for el in doc.iter():
Expand All @@ -340,7 +375,14 @@ def run(self, doc):
for inpt, label in zip(inputs, labels):
text = toc.get_name(label)
innertext = toc.unescape(toc.stashedHTML2text(text, self.md))
slug = toc.unique(self.slugify(innertext, self.sep), used_ids)
if self.combine_header_slug:
parent_slug = self.get_parent_header_slug(doc, header_map, parent_map, el)
else:
parent_slug = ''
slug = self.slugify(innertext, self.sep)
if parent_slug:
slug = parent_slug + self.sep + slug
slug = toc.unique(slug, used_ids)
inpt.attrib["id"] = slug
label.attrib["for"] = slug

Expand All @@ -354,6 +396,7 @@ def __init__(self, *args, **kwargs):
self.config = {
'alternate_style': [False, "Use alternate style - Default: False"],
'slugify': [0, "Slugify function used to create tab specific IDs - Default: None"],
'combine_header_slug': [False, "Combine the tab slug with the slug of the parent header - Default: False"],
'separator': ['-', "Slug separator - Default: '-'"]
}

Expand Down
108 changes: 108 additions & 0 deletions tests/test_extensions/test_blocks/test_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,114 @@ def test_slug_with_separator(self):
)


class TestTabSlugsCombineHeader(util.MdCase):
"""Combine header slug with content tab."""

extension = ['pymdownx.blocks.tab', 'toc', 'pymdownx.blocks.details']
extension_configs = {
'pymdownx.blocks.tab': {
'slugify': slugify(case='lower'),
'combine_header_slug': True,
'alternate_style': True
}
}

def test_combine_header_slug(self):
"""Test that slugs are a combination of the header slug and the tab title."""

md = R"""
### Here is some text
/// tab | First Tab
content
///
### Another header
/// details | title
//// tab | Second Tab
content
////
///
"""

self.check_markdown(
md,
'''
<h3 id="here-is-some-text">Here is some text</h3>
<div class="tabbed-set tabbed-alternate" data-tabs="1:1"><input checked="checked" id="here-is-some-text-first-tab" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="here-is-some-text-first-tab">First Tab</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>content</p>
</div>
</div>
</div>
<h3 id="another-header">Another header</h3>
<details>
<summary>title</summary>
<div class="tabbed-set tabbed-alternate" data-tabs="2:1"><input checked="checked" id="another-header-second-tab" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="another-header-second-tab">Second Tab</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>content</p>
</div>
</div>
</div>
</details>
''', # noqa: E501
True
)

def test_no_header(self):
"""Test when there is no header."""

md = R"""
/// tab | A Tab
content
///
"""

self.check_markdown(
md,
'''
<div class="tabbed-set tabbed-alternate" data-tabs="1:1"><input checked="checked" id="a-tab" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="a-tab">A Tab</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>content</p>
</div>
</div>
</div>
''', # noqa: E501
True
)

def test_header_after(self):
"""Test when header comes after."""

md = R"""
/// tab | A Tab
content
///
# Header
"""

self.check_markdown(
md,
'''
<div class="tabbed-set tabbed-alternate" data-tabs="1:1"><input checked="checked" id="a-tab" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="a-tab">A Tab</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p>content</p>
</div>
</div>
</div>
<h1 id="header">Header</h1>
''', # noqa: E501
True
)


class TestBlocksTab(util.MdCase):
"""Test Blocks tab cases."""

Expand Down
Loading

0 comments on commit 5620778

Please sign in to comment.