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, }