304 lines
9.7 KiB
Python
304 lines
9.7 KiB
Python
"""Tabs."""
|
|
import xml.etree.ElementTree as etree
|
|
from markdown.extensions import toc
|
|
from markdown.treeprocessors import Treeprocessor
|
|
from .block import Block, type_boolean
|
|
from ..blocks import BlocksExtension
|
|
import html
|
|
|
|
HEADERS = {'h1', 'h2', 'h3', 'h4', 'h5', 'h6'}
|
|
|
|
|
|
class TabbedTreeprocessor(Treeprocessor):
|
|
"""Tab tree processor."""
|
|
|
|
def __init__(self, md, config):
|
|
"""Initialize."""
|
|
|
|
super().__init__(md)
|
|
|
|
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 = {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():
|
|
if isinstance(el.tag, str) and el.tag.lower() == 'div':
|
|
classes = el.attrib.get('class', '').split()
|
|
if 'tabbed-set' in classes and (not self.alternate or 'tabbed-alternate' in classes):
|
|
inputs = []
|
|
labels = []
|
|
if self.alternate:
|
|
for i in list(el):
|
|
if i.tag == 'input':
|
|
inputs.append(i)
|
|
if i.tag == 'div' and i.attrib.get('class', '') == 'tabbed-labels':
|
|
labels = [j for j in list(i) if j.tag == 'label']
|
|
else:
|
|
for i in list(el):
|
|
if i.tag == 'input':
|
|
inputs.append(i)
|
|
if i.tag == 'label':
|
|
labels.append(i)
|
|
|
|
# Generate slugged IDs
|
|
for inpt, label in zip(inputs, labels):
|
|
innerhtml = toc.render_inner_html(toc.remove_fnrefs(label), self.md)
|
|
innertext = html.unescape(toc.strip_tags(innerhtml))
|
|
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
|
|
|
|
|
|
class Tab(Block):
|
|
"""
|
|
Tabbed container.
|
|
|
|
Arguments (1 required):
|
|
- A tab title.
|
|
|
|
Options:
|
|
- `new` (boolean): since consecutive tabs are automatically grouped, `new` can force a tab
|
|
to start a new tab container.
|
|
|
|
Content:
|
|
Detail body.
|
|
"""
|
|
|
|
NAME = 'tab'
|
|
|
|
ARGUMENT = True
|
|
OPTIONS = {
|
|
'new': [False, type_boolean],
|
|
'select': [False, type_boolean]
|
|
}
|
|
|
|
def on_init(self):
|
|
"""Handle initialization."""
|
|
|
|
self.alternate_style = self.config['alternate_style']
|
|
self.slugify = callable(self.config['slugify'])
|
|
|
|
# Track tab group count across the entire page.
|
|
if 'tab_group_count' not in self.tracker:
|
|
self.tracker['tab_group_count'] = 0
|
|
|
|
self.tab_content = None
|
|
|
|
def last_child(self, parent):
|
|
"""Return the last child of an `etree` element."""
|
|
|
|
if len(parent):
|
|
return parent[-1]
|
|
else:
|
|
return None
|
|
|
|
def on_add(self, block):
|
|
"""Adjust where the content is added."""
|
|
|
|
if self.tab_content is None:
|
|
if self.alternate_style:
|
|
for d in block.findall('div'):
|
|
c = d.attrib['class']
|
|
if c == 'tabbed-content' or c.startswith('tabbed-content '):
|
|
self.tab_content = list(d)[-1]
|
|
break
|
|
else:
|
|
self.tab_content = list(block)[-1]
|
|
|
|
return self.tab_content
|
|
|
|
def on_create(self, parent):
|
|
"""Create the element."""
|
|
|
|
new_group = self.options['new']
|
|
select = self.options['select']
|
|
title = self.argument
|
|
sibling = self.last_child(parent)
|
|
tabbed_set = 'tabbed-set' if not self.alternate_style else 'tabbed-set tabbed-alternate'
|
|
index = 0
|
|
labels = None
|
|
content = None
|
|
|
|
if (
|
|
sibling is not None and sibling.tag.lower() == 'div' and
|
|
sibling.attrib.get('class', '') == tabbed_set and
|
|
not new_group
|
|
):
|
|
first = False
|
|
tab_group = sibling
|
|
|
|
if self.alternate_style:
|
|
index = [index for index, _ in enumerate(tab_group.findall('input'), 1)][-1]
|
|
for d in tab_group.findall('div'):
|
|
if d.attrib['class'] == 'tabbed-labels':
|
|
labels = d
|
|
elif d.attrib['class'] == 'tabbed-content':
|
|
content = d
|
|
if labels is not None and content is not None:
|
|
break
|
|
else:
|
|
first = True
|
|
self.tracker['tab_group_count'] += 1
|
|
tab_group = etree.SubElement(
|
|
parent,
|
|
'div',
|
|
{'class': tabbed_set, 'data-tabs': '%d:0' % self.tracker['tab_group_count']}
|
|
)
|
|
|
|
if self.alternate_style:
|
|
labels = etree.SubElement(
|
|
tab_group,
|
|
'div',
|
|
{'class': 'tabbed-labels'}
|
|
)
|
|
content = etree.SubElement(
|
|
tab_group,
|
|
'div',
|
|
{'class': 'tabbed-content'}
|
|
)
|
|
|
|
data = tab_group.attrib['data-tabs'].split(':')
|
|
tab_set = int(data[0])
|
|
tab_count = int(data[1]) + 1
|
|
|
|
attributes = {
|
|
"name": "__tabbed_%d" % tab_set,
|
|
"type": "radio"
|
|
}
|
|
|
|
if not self.slugify:
|
|
attributes['id'] = "__tabbed_%d_%d" % (tab_set, tab_count)
|
|
|
|
attributes2 = {"for": "__tabbed_%d_%d" % (tab_set, tab_count)} if not self.slugify else {}
|
|
|
|
if first or select:
|
|
attributes['checked'] = 'checked'
|
|
# Remove any previously assigned "checked states" to siblings
|
|
for i in tab_group.findall('input'):
|
|
if i.attrib.get('name', '') == f'__tabbed_{tab_set}':
|
|
if 'checked' in i.attrib:
|
|
del i.attrib['checked']
|
|
|
|
if self.alternate_style:
|
|
input_el = etree.Element(
|
|
'input',
|
|
attributes
|
|
)
|
|
tab_group.insert(index, input_el)
|
|
lab = etree.SubElement(
|
|
labels,
|
|
"label",
|
|
attributes2
|
|
)
|
|
lab.text = title
|
|
|
|
attrib = {'class': 'tabbed-block'}
|
|
etree.SubElement(
|
|
content,
|
|
"div",
|
|
attrib
|
|
)
|
|
else:
|
|
etree.SubElement(
|
|
tab_group,
|
|
'input',
|
|
attributes
|
|
)
|
|
lab = etree.SubElement(
|
|
tab_group,
|
|
"label",
|
|
attributes2
|
|
)
|
|
lab.text = title
|
|
|
|
etree.SubElement(
|
|
tab_group,
|
|
"div",
|
|
{
|
|
"class": "tabbed-content"
|
|
}
|
|
)
|
|
|
|
tab_group.attrib['data-tabs'] = '%d:%d' % (tab_set, tab_count)
|
|
|
|
return tab_group
|
|
|
|
|
|
class TabExtension(BlocksExtension):
|
|
"""Admonition Blocks Extension."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize."""
|
|
|
|
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: '-'"]
|
|
}
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def extendMarkdownBlocks(self, md, block_mgr):
|
|
"""Extend Markdown blocks."""
|
|
|
|
block_mgr.register(Tab, self.getConfigs())
|
|
if callable(self.getConfig('slugify')):
|
|
slugs = TabbedTreeprocessor(md, self.getConfigs())
|
|
md.treeprocessors.register(slugs, 'tab_slugs', 4)
|
|
|
|
|
|
def makeExtension(*args, **kwargs):
|
|
"""Return extension."""
|
|
|
|
return TabExtension(*args, **kwargs)
|