sphinx-structured-toc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ from pathlib import Path
2
+
3
+ from sphinx.application import Sphinx
4
+ from sphinx.util.typing import ExtensionMetadata
5
+
6
+ from .directives import DomainDirective, SliceDirective
7
+ from .html import (
8
+ depart_domain,
9
+ depart_slice,
10
+ depart_slice_item,
11
+ visit_domain,
12
+ visit_slice,
13
+ visit_slice_item,
14
+ )
15
+ from .nodes import Domain, Slice, SliceItem
16
+ from .transforms import resolve_domains
17
+
18
+ _CSS_DIR = str(Path(__file__).parent)
19
+
20
+ try:
21
+ from ._version import __version__
22
+ except ImportError: # pragma: no cover
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ try:
26
+ __version__ = version("sphinx-structured-toc")
27
+ except PackageNotFoundError:
28
+ __version__ = "dev"
29
+
30
+
31
+ def setup(app: Sphinx) -> ExtensionMetadata:
32
+ # register various components
33
+
34
+ # nodes
35
+ app.add_node(Domain, html=(visit_domain, depart_domain))
36
+ app.add_node(Slice, html=(visit_slice, depart_slice))
37
+ app.add_node(SliceItem, html=(visit_slice_item, depart_slice_item))
38
+
39
+ # directives
40
+ app.add_directive("domain", DomainDirective)
41
+ app.add_directive("slice", SliceDirective)
42
+
43
+ # transform
44
+ app.connect("doctree-resolved", resolve_domains)
45
+
46
+ # static asset shipping
47
+ app.connect("builder-inited", add_static_dir)
48
+
49
+ # reference override (decorates <a> emitted for marked slice items)
50
+ app.connect("builder-inited", install_reference_override)
51
+
52
+ app.add_css_file("domain-list.css")
53
+
54
+ return {
55
+ "version": __version__,
56
+ "parallel_read_safe": True,
57
+ "parallel_write_safe": True,
58
+ }
59
+
60
+
61
+ def add_static_dir(app: Sphinx) -> None:
62
+ """Add the extension package directory to ``html_static_path``."""
63
+
64
+ if _CSS_DIR not in app.config.html_static_path:
65
+ app.config.html_static_path.append(_CSS_DIR)
66
+
67
+
68
+ def install_reference_override(app: Sphinx) -> None:
69
+ """Override ``docutils.nodes.reference`` HTML visitors."""
70
+
71
+ from docutils import nodes as docutils_nodes
72
+
73
+ from .html import make_reference_visitor
74
+
75
+ visit, depart = make_reference_visitor(app)
76
+ app.add_node(
77
+ docutils_nodes.reference,
78
+ html=(visit, depart),
79
+ override=True,
80
+ )
81
+
82
+
83
+ __all__ = ["__version__", "setup"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = 'g8833e90'
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from docutils import nodes
6
+ from docutils.parsers.rst.directives import flag
7
+ from sphinx.addnodes import pending_xref
8
+ from sphinx.util.docutils import SphinxDirective
9
+ from sphinx.util.typing import OptionSpec
10
+
11
+ from .nodes import Domain, Slice, SliceItem
12
+
13
+
14
+ class DomainDirective(SphinxDirective):
15
+ """Outer directive: ``.. domain::`` with an optional name argument."""
16
+
17
+ optional_arguments = 1
18
+ final_argument_whitespace = True # allow whitespace in the name
19
+ has_content = True # has content that will need to be parsed
20
+ option_spec: ClassVar[OptionSpec] = {"suppress-warnings": flag}
21
+
22
+ def run(self) -> list[nodes.Node]:
23
+ """Parse nested content and validate that only Slice children result."""
24
+ overridden = bool(self.arguments)
25
+
26
+ # if not supplied the name will need to be resolved later
27
+ name = self.arguments[0] if overridden else ""
28
+
29
+ nested = nodes.Element()
30
+ # parse the inner content
31
+ self.state.nested_parse(self.content, self.content_offset, nested)
32
+
33
+ # if slice parsing fails, forward the first error
34
+ messages = [c for c in nested.children if isinstance(c, nodes.system_message)]
35
+ if messages:
36
+ return [messages[0]]
37
+
38
+ slices = [child for child in nested.children if isinstance(child, Slice)]
39
+ non_slice = [child for child in nested.children if not isinstance(child, Slice)]
40
+
41
+ if non_slice:
42
+ return [self.emit_error("domain may only contain 'slice' blocks")]
43
+
44
+ if not slices:
45
+ return [self.emit_error("domain must contain at least one slice")]
46
+
47
+ # create the node, pass its attributes, add the slices
48
+ domain_node = Domain()
49
+ if overridden:
50
+ domain_node["name"] = name
51
+ domain_node["overridden"] = overridden
52
+ if "suppress-warnings" in self.options:
53
+ domain_node["suppress_warnings"] = True
54
+ domain_node.extend(slices)
55
+ return [domain_node]
56
+
57
+ def emit_error(self, msg: str) -> nodes.system_message:
58
+ return self.state_machine.reporter.error(
59
+ msg,
60
+ nodes.literal_block(self.block_text, self.block_text),
61
+ line=self.lineno,
62
+ )
63
+
64
+
65
+ class SliceDirective(SphinxDirective):
66
+ """Inner directive: ``.. slice:: <name>`` with item lines as content."""
67
+
68
+ required_arguments = 1
69
+ final_argument_whitespace = True
70
+ has_content = True
71
+ option_spec: dict = {}
72
+
73
+ def run(self) -> list[nodes.Node]:
74
+ """Build a Slice node from the directive content."""
75
+
76
+ name = self.arguments[0]
77
+
78
+ slice_node = Slice()
79
+ slice_node["name"] = name
80
+
81
+ # adds parsed lines into the slice_node
82
+ for offset, line in enumerate(self.content):
83
+ if not line.strip():
84
+ continue
85
+ lineno = self.lineno + self.content_offset + offset
86
+ item = self.parse_item(line, lineno)
87
+ if isinstance(item, nodes.system_message):
88
+ return [item]
89
+ slice_node.append(item)
90
+
91
+ if not slice_node.children:
92
+ return [self.emit_error(f"slice '{name}' has no items")]
93
+
94
+ return [slice_node]
95
+
96
+ def parse_item(self, line: str, lineno: int) -> nodes.Node:
97
+ role_text, mark_slice, mark_domain, err = self.parse_line(line)
98
+ if err is not None:
99
+ return self.emit_error(err, lineno=lineno)
100
+
101
+ nodes_list, messages = self.state.inline_text(role_text, lineno)
102
+
103
+ if messages:
104
+ return self.emit_error(
105
+ f"slice item must be a :doc: role: {line!r}",
106
+ lineno=lineno,
107
+ )
108
+
109
+ # each line should contain one and only cross-reference and nothing else
110
+ xrefs = [n for n in nodes_list if isinstance(n, pending_xref)]
111
+ non_xrefs = [
112
+ n
113
+ for n in nodes_list
114
+ if not isinstance(n, pending_xref)
115
+ and not (isinstance(n, nodes.Text) and not str(n).strip())
116
+ ]
117
+
118
+ if len(xrefs) != 1 or non_xrefs:
119
+ return self.emit_error(
120
+ f"slice item must be a :doc: role: {line!r}",
121
+ lineno=lineno,
122
+ )
123
+
124
+ xref = xrefs[0]
125
+ if xref.get("refdomain") != "std" or xref.get("reftype") != "doc":
126
+ return self.emit_error(
127
+ f"slice item must be a :doc: role: {line!r}",
128
+ lineno=lineno,
129
+ )
130
+
131
+ # create a SliceItem from the line
132
+ item = SliceItem(rawsource=line)
133
+ if mark_slice:
134
+ item["mark_slice"] = True
135
+ if mark_domain:
136
+ item["mark_domain"] = True
137
+ item += xref
138
+ return item
139
+
140
+ def parse_line(self, line: str) -> tuple[str, bool, bool, str | None]:
141
+ """Parses a line into its components"""
142
+
143
+ mark_slice = mark_domain = False
144
+ text = line.rstrip()
145
+ parts = text.split()
146
+
147
+ while parts and parts[-1] in ("slice", "domain"):
148
+ marker = parts.pop()
149
+ if marker == "slice" and not mark_slice:
150
+ mark_slice = True
151
+ elif marker == "domain" and not mark_domain:
152
+ mark_domain = True
153
+ else:
154
+ parts.append(marker) # duplicate, hand back to error path
155
+ break
156
+
157
+ if parts and parts[-1] in ("slice", "domain"):
158
+ return (
159
+ line,
160
+ False,
161
+ False,
162
+ f"unrecognised trailing token in slice item: {line!r}",
163
+ )
164
+
165
+ role_text = " ".join(parts)
166
+ return role_text, mark_slice, mark_domain, None
167
+
168
+ def emit_error(self, msg: str, lineno: int | None = None) -> nodes.system_message:
169
+ return self.state_machine.reporter.error(
170
+ msg,
171
+ nodes.literal_block(self.block_text, self.block_text),
172
+ line=lineno if lineno is not None else self.lineno,
173
+ )
@@ -0,0 +1,29 @@
1
+ .domain-list > ul > li > ul {
2
+ display: inline;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ .domain-list > ul > li > ul > li {
8
+ display: inline;
9
+ padding-left: 0.3em;
10
+ border-left: 1px solid currentColor;
11
+ margin-left: 0.3em;
12
+ }
13
+
14
+ .domain-list > ul > li > ul > li:first-child {
15
+ border-left: none;
16
+ margin-left: 0;
17
+ padding-left: 0;
18
+ }
19
+
20
+ .domain-list-label {font-weight: bold;}
21
+
22
+ .domain-aria-target {
23
+ position: absolute;
24
+ width: 1px;
25
+ height: 1px;
26
+ overflow: hidden;
27
+ clip: rect(0, 0, 0, 0);
28
+ white-space: nowrap;
29
+ }
@@ -0,0 +1,190 @@
1
+ """HTML visitors for sphinx-structured-toc nodes (Phase 8).
2
+
3
+ Renders the annotated doctree as semantic HTML:
4
+
5
+ * ``Domain`` becomes ``<nav aria-labelledby="...">`` referencing either the
6
+ enclosing section's heading id (when the domain name is derived from a
7
+ section heading) or a visually-hidden ``<span id="..." class="domain-
8
+ aria-target">`` holding the name (when the domain name is overridden).
9
+ The nameless case (no argument and no enclosing section) is a fatal
10
+ build error, raised in ``transforms.py`` before rendering.
11
+ * ``Slice`` becomes a ``<li>`` containing the slice label ``<span>`` plus
12
+ ``": "`` and a nested ``<ul>`` of items.
13
+ * ``SliceItem`` becomes a ``<li>`` containing the resolved ``<a>``.
14
+
15
+ For items marked ``slice`` and/or ``domain``, the ``<a>`` element
16
+ receives ``id`` and ``aria-labelledby``. The ``<a>`` is emitted by
17
+ Sphinx's own ``visit_reference``; this extension overrides that visitor
18
+ (globally, via ``app.add_node(..., override=True)``) so that when the
19
+ reference's parent is a ``SliceItem`` with the relevant flags set, the
20
+ aria/id attributes are injected. All other references fall through to
21
+ Sphinx's default behaviour.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ from docutils import nodes
29
+
30
+
31
+ def visit_domain(translator: Any, node: Any) -> None:
32
+ """Open the outer container for a Domain.
33
+
34
+ Emits ``<nav aria-labelledby="{section_id}">`` when the domain name
35
+ is derived from a section heading (not overridden), or
36
+ ``<nav aria-labelledby="{domain_span_id}">`` referencing a
37
+ visually-hidden ``<span id="{domain_span_id}" class="domain-aria-
38
+ target">{name}</span>`` when overridden. The nameless case (no
39
+ argument and no enclosing section) is a fatal build error raised in
40
+ ``transforms.py`` and never reaches the visitor.
41
+ """
42
+ overridden = bool(node.get("overridden", False))
43
+ section_id = node.get("section_id", "")
44
+ name = node.get("name", "")
45
+
46
+ if overridden:
47
+ # aria-labelledby on the nav points at a visually-hidden span
48
+ # inside, which is the single source of truth for the name and
49
+ # is also referenced by aria-labelledby on marked items.
50
+ span_id = node.get("domain_span_id", "")
51
+ translator.body.append(
52
+ f'<nav class="domain-list" aria-labelledby="{span_id}">'
53
+ )
54
+ # "domain-aria-target" visually hides the span while keeping it
55
+ # in the accessibility tree (styles in domain-list.css).
56
+ translator.body.append(
57
+ f'<span id="{span_id}" class="domain-aria-target">'
58
+ f'{translator.attval(name)}</span>'
59
+ )
60
+ else:
61
+ translator.body.append(
62
+ f'<nav class="domain-list" aria-labelledby="{section_id}">'
63
+ )
64
+
65
+ # One top-level <ul> for the slices.
66
+ translator.body.append("<ul>")
67
+
68
+
69
+ def depart_domain(translator: Any, node: Any) -> None:
70
+ """Close the outer container for a Domain."""
71
+ translator.body.append("</ul>")
72
+ translator.body.append("</nav>")
73
+
74
+
75
+ def visit_slice(translator: Any, node: Any) -> None:
76
+ """Open a slice as a <li> containing the label <span> and a nested <ul>.
77
+
78
+ The colon and trailing space sit in the parent <li>, after the
79
+ ``<span>``; not inside the span, not in CSS.
80
+ """
81
+ label_id = node.get("label_id", "")
82
+ name = node.get("name", "")
83
+ translator.body.append("<li>")
84
+ translator.body.append(
85
+ f'<span id="{label_id}" class="domain-list-label">'
86
+ f"{translator.attval(name)}</span>: "
87
+ )
88
+ translator.body.append("<ul>")
89
+
90
+
91
+ def depart_slice(translator: Any, node: Any) -> None:
92
+ """Close a slice's nested <ul> and the slice <li>."""
93
+ translator.body.append("</ul></li>")
94
+
95
+
96
+ def visit_slice_item(translator: Any, node: Any) -> None:
97
+ """Open a slice item as a <li>.
98
+
99
+ The ``<a>`` itself is emitted by Sphinx's ``visit_reference`` (which
100
+ we override to inject ``id``/``aria-labelledby`` when the parent
101
+ ``SliceItem`` is marked). Here we only open the wrapping ``<li>``.
102
+ """
103
+ translator.body.append("<li>")
104
+
105
+
106
+ def depart_slice_item(translator: Any, node: Any) -> None:
107
+ """Close a slice item's <li>."""
108
+ translator.body.append("</li>")
109
+
110
+
111
+ def make_reference_visitor(app: Any) -> tuple[Any, Any]:
112
+ """Build ``visit``/``depart`` overrides for ``docutils.nodes.reference``.
113
+
114
+ Returns ``(visit_reference, depart_reference)``. The visit function
115
+ delegates to Sphinx's original ``visit_reference`` for any reference
116
+ whose parent is not a marked ``SliceItem``. For marked ``SliceItem``
117
+ parents it injects ``id`` and ``aria-labelledby`` onto the ``<a>``
118
+ before delegating, so the rest of Sphinx's reference handling
119
+ (classes, href, secnumber, etc.) is preserved.
120
+
121
+ The override is registered globally via
122
+ ``app.add_node(docutils.nodes.reference, html=(...), override=True)``.
123
+ """
124
+ from sphinx.writers.html5 import HTML5Translator
125
+
126
+ from .nodes import SliceItem
127
+
128
+ original_visit = HTML5Translator.visit_reference
129
+ original_depart = getattr(HTML5Translator, "depart_reference", None)
130
+
131
+ def visit_reference(self: Any, node: nodes.reference) -> None:
132
+ parent = node.parent
133
+ if isinstance(parent, SliceItem) and (
134
+ parent.get("mark_slice") or parent.get("mark_domain")
135
+ ):
136
+ item_id = parent.get("item_id", "")
137
+ slice_node = parent.parent
138
+ domain_node = slice_node.parent if slice_node is not None else None
139
+ labelledby: list[str] = []
140
+ if item_id:
141
+ labelledby.append(item_id)
142
+ if parent.get("mark_slice") and slice_node is not None:
143
+ slice_label_id = slice_node.get("label_id", "")
144
+ if slice_label_id:
145
+ labelledby.append(slice_label_id)
146
+ if parent.get("mark_domain") and domain_node is not None:
147
+ if domain_node.get("overridden", False):
148
+ domain_id = domain_node.get("domain_span_id", "")
149
+ else:
150
+ domain_id = domain_node.get("section_id", "")
151
+ if domain_id:
152
+ labelledby.append(domain_id)
153
+ if item_id:
154
+ # ``starttag`` will emit the id from ``node['ids']``.
155
+ node.setdefault("ids", []).append(item_id)
156
+ # Delegate to Sphinx's visit_reference, then patch the
157
+ # opening tag it emitted to add aria-labelledby (Sphinx does
158
+ # not emit aria-* attributes on references by default).
159
+ mark = len(self.body)
160
+ original_visit(self, node)
161
+ if labelledby:
162
+ inject_aria_labelledby(self.body, mark, " ".join(labelledby))
163
+ return
164
+
165
+ original_visit(self, node)
166
+
167
+ def depart_reference(self: Any, node: nodes.reference) -> None:
168
+ if original_depart is not None:
169
+ original_depart(self, node)
170
+
171
+ return visit_reference, depart_reference
172
+
173
+
174
+ def inject_aria_labelledby(body: list[str], since: int, value: str) -> None:
175
+ """Inject ``aria-labelledby="..."`` into the most recent ``<a ...>`` tag.
176
+
177
+ Sphinx's ``visit_reference`` appends the opening ``<a>`` tag as a
178
+ single string via ``self.starttag``. We locate that string in
179
+ ``body`` (searching backwards from the end, starting at ``since``)
180
+ and insert the attribute before the closing ``>``.
181
+ """
182
+ for i in range(len(body) - 1, since - 1, -1):
183
+ chunk = body[i]
184
+ if "<a " in chunk or chunk.startswith("<a"):
185
+ # Insert before the closing ">" of the opening tag.
186
+ idx = chunk.rfind(">")
187
+ if idx == -1:
188
+ continue
189
+ body[i] = chunk[:idx] + f' aria-labelledby="{value}"' + chunk[idx:]
190
+ return
@@ -0,0 +1,13 @@
1
+ from docutils import nodes
2
+
3
+
4
+ class Domain(nodes.General, nodes.Element):
5
+ """Outer container for a ``.. domain::`` block."""
6
+
7
+
8
+ class Slice(nodes.General, nodes.Element):
9
+ """A single named slice within a ``Domain``."""
10
+
11
+
12
+ class SliceItem(nodes.General, nodes.TextElement):
13
+ """A single link entry within a ``Slice``."""
@@ -0,0 +1,175 @@
1
+ """Post-transform resolution for the sphinx-structured-toc extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from docutils import nodes
6
+ from sphinx.util.logging import getLogger
7
+
8
+ from .nodes import Domain, Slice, SliceItem
9
+
10
+ _logger = getLogger(__name__)
11
+
12
+
13
+ def nearest_section(node: nodes.Node) -> nodes.section | None:
14
+ """Return the nearest enclosing ``nodes.section`` for ``node``, or ``None``."""
15
+ parent = node.parent
16
+ while parent is not None:
17
+ if isinstance(parent, nodes.section):
18
+ return parent
19
+ parent = parent.parent
20
+ return None
21
+
22
+
23
+ def section_title(section: nodes.section) -> str:
24
+ """Return the text of a section's first ``nodes.title`` child, or ``""``."""
25
+ title = section.next_node(nodes.title)
26
+ return title.astext() if title is not None else ""
27
+
28
+
29
+ def section_id(section: nodes.section) -> str:
30
+ """Return the section's first id, or ``""`` when it has none."""
31
+ ids = section.get("ids", [])
32
+ return ids[0] if ids else ""
33
+
34
+
35
+ def visible_text(item: SliceItem) -> str:
36
+ """Return the visible text of a SliceItem's resolved ``:doc:`` reference."""
37
+
38
+ from docutils import nodes as _nodes
39
+
40
+ for child in item.children:
41
+ if isinstance(child, _nodes.reference):
42
+ return child.astext()
43
+ return ""
44
+
45
+
46
+ def accessible_name(item: SliceItem, slice_node: Slice, domain: Domain) -> str:
47
+ """Return the accessible name of a marked item."""
48
+
49
+ parts: list[str] = [visible_text(item)]
50
+ if item.get("mark_slice"):
51
+ parts.append(slice_node["name"])
52
+ if item.get("mark_domain"):
53
+ parts.append(domain["name"])
54
+ return " ".join(parts)
55
+
56
+
57
+ def check_ambiguity(doctree) -> None:
58
+ """Warn at each occurrence of repeated visible text with identical accessible names."""
59
+
60
+ # Group items by visible text across the whole page, recording the
61
+ # accessible name and the slice/domain each came from so the
62
+ # same-slice special case can be detected.
63
+ occurrences: dict[str, list[tuple[SliceItem, Slice, Domain]]] = {}
64
+ for domain in doctree.findall(Domain):
65
+ if domain.get("suppress_warnings", False):
66
+ continue
67
+ for slice_node in domain.findall(Slice):
68
+ for item in slice_node.children:
69
+ if not isinstance(item, SliceItem):
70
+ continue
71
+ text = visible_text(item)
72
+ if not text:
73
+ continue
74
+ occurrences.setdefault(text, []).append((item, slice_node, domain))
75
+
76
+ for text, group in occurrences.items():
77
+ if len(group) < 2:
78
+ continue
79
+
80
+ # Same-slice special case: any two items in the same slice
81
+ # instance with the same visible text always warn.
82
+ for i, (item_i, slice_i, _dom_i) in enumerate(group):
83
+ for j, (item_j, slice_j, _dom_j) in enumerate(group):
84
+ if j <= i:
85
+ continue
86
+ if slice_i is slice_j:
87
+ _logger.warning(
88
+ "ambiguous link text %r: repeated within slice %r",
89
+ text,
90
+ slice_i["name"],
91
+ )
92
+ continue
93
+ # Cross-slice: warn unless accessible names differ.
94
+ name_i = accessible_name(item_i, slice_i, _dom_i)
95
+ name_j = accessible_name(item_j, slice_j, _dom_j)
96
+ if name_i == name_j:
97
+ _logger.warning(
98
+ "ambiguous link text %r: identical accessible "
99
+ "name %r across slices %r and %r",
100
+ text,
101
+ name_i,
102
+ slice_i["name"],
103
+ slice_j["name"],
104
+ )
105
+
106
+
107
+ def unique_id(base: str, used: set[str]) -> str:
108
+ """Return ``base`` if unused, else ``base-2``, ``base-3``, ... on clash."""
109
+
110
+ candidate = base
111
+ counter = 2
112
+ while candidate in used:
113
+ candidate = f"{base}-{counter}"
114
+ counter += 1
115
+ used.add(candidate)
116
+ return candidate
117
+
118
+
119
+ def resolve_domains(_app, doctree, _docname) -> None:
120
+ """``doctree-resolved`` handler: fill in name/section_id and assign ids."""
121
+ used_ids: set[str] = set()
122
+
123
+ # Seed with ids already present on the page (section heading ids,
124
+ # etc.) so generated ids never collide with existing ones.
125
+ for node in doctree.findall(nodes.Element):
126
+ for existing in node.get("ids", []):
127
+ used_ids.add(existing)
128
+
129
+ for domain in doctree.findall(Domain):
130
+ section = nearest_section(domain)
131
+ overridden = domain.get("overridden", False)
132
+ if section is not None:
133
+ domain["section_id"] = section_id(section)
134
+ if not overridden:
135
+ domain["name"] = section_title(section)
136
+ else:
137
+ domain["section_id"] = ""
138
+ if not overridden:
139
+ # No name available from anywhere: an unlabelled landmark
140
+ # is worse for screen reader users than no landmark at all,
141
+ # so fail the build rather than degrading to a nameless
142
+ # container.
143
+ _logger.error(
144
+ "domain directive requires an explicit name argument "
145
+ "or an enclosing section heading to derive one from"
146
+ )
147
+ continue
148
+
149
+ section_slug = domain["section_id"]
150
+
151
+ if domain.get("overridden", False):
152
+ name_slug = nodes.make_id(domain["name"])
153
+ domain["domain_span_id"] = unique_id(f"{name_slug}-domain", used_ids)
154
+
155
+ for slice_node in domain.findall(Slice):
156
+ slice_slug = nodes.make_id(slice_node["name"])
157
+ if section_slug:
158
+ base = f"{section_slug}-{slice_slug}"
159
+ else:
160
+ base = slice_slug
161
+ slice_node["label_id"] = unique_id(base, used_ids)
162
+
163
+ for item in slice_node.children:
164
+ if not isinstance(item, SliceItem):
165
+ continue
166
+ if not (item.get("mark_slice") or item.get("mark_domain")):
167
+ continue
168
+ link_text = visible_text(item)
169
+ if not link_text:
170
+ continue
171
+ link_slug = nodes.make_id(link_text)
172
+ base = f"{slice_node['label_id']}-{link_slug}"
173
+ item["item_id"] = unique_id(base, used_ids)
174
+
175
+ check_ambiguity(doctree)