Source code for sphinx_better_subsection
"""Sphinx extension to prefer explicit IDs in sections
Add this extension using::
extensions += ["sphinx_better_subsection"]
Using the transformer directly is also allowed with::
from sphinx_better_subsection import PreferSectionTarget
app.add_transform(PreferSectionTarget)
"""
from docutils import nodes
from docutils.transforms import Transform
[docs]
class PreferSectionTarget(Transform):
"""Prefer target IDs over the section's own
Given this input text:
.. code-block:: rest
.. _a:
.. _b:
.. _c:
Section Title
-------------
A paragraph.
This parses into:
.. code-block:: xml
...
<target ids="['a']" names="['a']">
<target ids="['b']" names="['b']">
<target ids="['c']" names="['c']">
<section ids="section-title" names="Section\\ Title">
...
Transforming it gives:
.. code-block:: xml
...
<target ids="['a']" names="['a']">
<target ids="['b']" names="['b']">
<target refid="c">
<section ids="c section-title" names="c Section\\ Title">
...
Note that the other IDs are all preserved; only the order is modified.
Nested subsections are also checked.
"""
# Run before `docutils.transforms.references.PropagateTransform` which has
# priority 260.
default_priority = 255
[docs]
def apply(self):
"""Docutils transform entry point"""
# `.findall` is new in docutils 0.18. Fallback to `.traverse`.
try:
findall = self.document.findall
except AttributeError:
findall = self.document.traverse
for node in findall(nodes.section):
# Get node directly preceding the section
index = node.parent.index(node)
if index == 0:
continue
last = node.parent[index - 1]
# Targets are part of the previous section so we should look deeper
while isinstance(last, nodes.Node) and last.children:
last = last[-1]
# Filter away nodes that aren't targets
if not isinstance(last, nodes.target):
continue
# Filter away targets with content
if (
isinstance(last.parent, nodes.TextElement)
or last.hasattr("refid")
or last.hasattr("refuri")
or last.hasattr("refname")
):
continue
# Store ID and name
refname = last["names"][0]
refid = last["ids"][0]
# Propagate the previous target
# Source from `PropagateTargets.apply`
node["ids"].extend(last["ids"])
node["names"].extend(last["names"])
# Set defaults for node.expect_referenced_by_name/id.
if not hasattr(node, "expect_referenced_by_name"):
node.expect_referenced_by_name = {}
if not hasattr(node, "expect_referenced_by_id"):
node.expect_referenced_by_id = {}
for id_ in last["ids"]:
node.expect_referenced_by_id[id_] = last
# Update IDs to node mapping.
self.document.ids[id_] = node
last["ids"] = []
for name in last["names"]:
node.expect_referenced_by_name[name] = last
last["names"] = []
# If there are any expect_referenced_by_name/id attributes in
# target set, copy them to node.
node.expect_referenced_by_name.update(
getattr(last, "expect_referenced_by_name", {}))
node.expect_referenced_by_id.update(
getattr(last, "expect_referenced_by_id", {}))
# Set refid to point to the first former ID of target which is now
# an ID of next_node.
last["refid"] = refid
self.document.note_refid(last)
# End source
# Prefer the target's ID
node["ids"].remove(refid)
node["ids"].insert(0, refid)
# Also prefer the target's name
node["names"].remove(refname)
node["names"].insert(0, refname)
[docs]
def setup(app):
"""Sphinx extension entry point"""
app.add_transform(PreferSectionTarget)
return {
# Probably parallel-read safe (though I'm not sure)
"parallel_read_safe": True,
}