From 0cf4a3eab9c3e3e54df23532d70b89ba4109e1f7 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 14:36:38 +0800 Subject: [PATCH 1/4] fix: strip markup for ref --- docs/_ext/rule_directive.py | 50 +++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/docs/_ext/rule_directive.py b/docs/_ext/rule_directive.py index feaee7a..4f88d26 100644 --- a/docs/_ext/rule_directive.py +++ b/docs/_ext/rule_directive.py @@ -23,6 +23,7 @@ from typing import cast from docutils import nodes +from docutils.core import publish_doctree from sphinx.addnodes import pending_xref from sphinx.domains import Domain, ObjType from sphinx.roles import XRefRole @@ -71,18 +72,9 @@ def run(self): rule_code, rule_name = self.arguments rule_code = rule_code.upper() - ref_name = "tg.rule." + rule_code.lower() - node_id = nodes.make_id(ref_name) - - # register the label in Sphinx's standard domain - std_domain = self.env.domains.standard_domain - std_domain.note_hyperlink_target( - ref_name, self.env.docname, node_id, f"{rule_code} ({rule_name})" - ) - # register the rule in tugboat domain tugboat_domain = cast("TugboatDomain", self.env.get_domain("tg")) - tugboat_domain.note_rule(rule_code, rule_name, node_id) + node_id = tugboat_domain.note_rule(rule_code, rule_name) # create section and populate it with title and content rule_name_nodes, _ = self.parse_inline(rule_name) @@ -166,13 +158,43 @@ class TugboatDomain(Domain): def rules(self) -> dict[str, RuleEntry]: return self.data.setdefault("rules", {}) - def note_rule(self, rule_code: str, rule_name: str, node_id: str): + def note_rule(self, rule_code: str, rule_name: str) -> str: + """ + Note a rule in the domain. + + Parameters + ---------- + rule_code : str + The unique code for the rule. + rule_name : str + The name of the rule. + + Returns + ------- + node_id : str + The node ID for the rule target. + """ + ref_name = "tg.rule." + rule_code.lower() + node_id = nodes.make_id(ref_name) + + # register a rule in this domain self.rules[rule_code.upper()] = RuleEntry( doc_name=self.env.docname, node_id=node_id, rule_name=rule_name, ) + # register the label in Sphinx's standard domain + std_domain = self.env.domains.standard_domain + std_domain.note_hyperlink_target( + name=ref_name, + docname=self.env.docname, + node_id=node_id, + title=f"{rule_code} ({strip_rst(rule_name)})", + ) + + return node_id + def get_objects(self): for label, (doc_name, node_id, rule_name) in self.rules.items(): yield (label, rule_name, "rule", doc_name, node_id, 1) @@ -210,3 +232,9 @@ def resolve_xref( data.node_id, ) return None + + +def strip_rst(text: str) -> str: + """Strip reStructuredText markup from the given text.""" + doctree = publish_doctree(text) + return doctree.astext() From 3168b41b40249b52c95abad16d31c6db00ca72d8 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 16:11:35 +0800 Subject: [PATCH 2/4] enh: use dataclass --- docs/_ext/rule_directive.py | 50 ++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/_ext/rule_directive.py b/docs/_ext/rule_directive.py index 4f88d26..2bdd812 100644 --- a/docs/_ext/rule_directive.py +++ b/docs/_ext/rule_directive.py @@ -19,7 +19,9 @@ from __future__ import annotations +import functools import typing +from dataclasses import dataclass from typing import cast from docutils import nodes @@ -54,10 +56,23 @@ def setup(app: Sphinx): } -class RuleEntry(typing.NamedTuple): +@dataclass(frozen=True) +class RuleEntry: + + code: str + title: str doc_name: str - node_id: str - rule_name: str + + def __post_init__(self): + object.__setattr__(self, "code", self.code.upper()) + + @functools.cached_property + def ref_name(self) -> str: + return "tg.rule." + self.code.lower() + + @functools.cached_property + def node_id(self) -> str: + return nodes.make_id(self.ref_name) class RuleDirective(SphinxDirective): @@ -74,7 +89,7 @@ def run(self): # register the rule in tugboat domain tugboat_domain = cast("TugboatDomain", self.env.get_domain("tg")) - node_id = tugboat_domain.note_rule(rule_code, rule_name) + entry = tugboat_domain.note_rule(rule_code, rule_name) # create section and populate it with title and content rule_name_nodes, _ = self.parse_inline(rule_name) @@ -90,7 +105,7 @@ def run(self): section += title section += self.parse_content_to_nodes() - target = nodes.target(ids=[node_id]) + target = nodes.target(ids=[entry.node_id]) return [target, section] @@ -158,7 +173,7 @@ class TugboatDomain(Domain): def rules(self) -> dict[str, RuleEntry]: return self.data.setdefault("rules", {}) - def note_rule(self, rule_code: str, rule_name: str) -> str: + def note_rule(self, rule_code: str, rule_name: str) -> RuleEntry: """ Note a rule in the domain. @@ -174,30 +189,29 @@ def note_rule(self, rule_code: str, rule_name: str) -> str: node_id : str The node ID for the rule target. """ - ref_name = "tg.rule." + rule_code.lower() - node_id = nodes.make_id(ref_name) - # register a rule in this domain - self.rules[rule_code.upper()] = RuleEntry( + entry = RuleEntry( + code=rule_code, + title=rule_name, doc_name=self.env.docname, - node_id=node_id, - rule_name=rule_name, ) + self.rules[rule_code.upper()] = entry + # register the label in Sphinx's standard domain std_domain = self.env.domains.standard_domain std_domain.note_hyperlink_target( - name=ref_name, + name=entry.ref_name, docname=self.env.docname, - node_id=node_id, + node_id=entry.node_id, title=f"{rule_code} ({strip_rst(rule_name)})", ) - return node_id + return entry def get_objects(self): - for label, (doc_name, node_id, rule_name) in self.rules.items(): - yield (label, rule_name, "rule", doc_name, node_id, 1) + for label, rule in self.rules.items(): + yield (label, rule.title, "rule", rule.doc_name, rule.node_id, 1) def clear_doc(self, docname: str): to_remove = [] @@ -228,7 +242,7 @@ def resolve_xref( fromdocname, data.doc_name, data.node_id, - make_rule_contnode(target, data.rule_name), + make_rule_contnode(target, data.title), data.node_id, ) return None From dbf9fab6947d3136bad733f26bf3ff6b0d68ed3e Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 16:55:59 +0800 Subject: [PATCH 3/4] fix: parse title --- docs/_ext/rule_directive.py | 73 ++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/docs/_ext/rule_directive.py b/docs/_ext/rule_directive.py index 2bdd812..b6f8a9d 100644 --- a/docs/_ext/rule_directive.py +++ b/docs/_ext/rule_directive.py @@ -22,10 +22,14 @@ import functools import typing from dataclasses import dataclass +from types import SimpleNamespace from typing import cast +import docutils.frontend from docutils import nodes -from docutils.core import publish_doctree +from docutils.parsers.rst import Parser +from docutils.parsers.rst.states import Inliner +from docutils.utils import new_document from sphinx.addnodes import pending_xref from sphinx.domains import Domain, ObjType from sphinx.roles import XRefRole @@ -36,7 +40,7 @@ from collections.abc import Set from typing import Any, ClassVar - from docutils.nodes import Element, Node, document, system_message + from docutils.nodes import Element, Node, document, reference, system_message from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment @@ -74,6 +78,30 @@ def ref_name(self) -> str: def node_id(self) -> str: return nodes.make_id(self.ref_name) + def title_nodes(self) -> list[Node]: + settings = docutils.frontend.get_default_settings(Parser()) + doc = new_document("", settings=settings) + + inliner = Inliner() + inliner.init_customizations(settings) + memo = SimpleNamespace(document=doc, reporter=doc.reporter, language=None) + + text_nodes, _ = inliner.parse(self.title, 1, memo, doc) + return text_nodes + + def title_stripped(self) -> str: + return "".join(node.astext() for node in self.title_nodes()) + + def node(self): + """Create reference node for this rule.""" + container = nodes.inline(classes=["inline-rule-ref"]) + container += [ + nodes.inline(text=self.code, classes=["rule-code"]), + nodes.Text(" "), + *self.title_nodes(), + ] + return container + class RuleDirective(SphinxDirective): """Directive for defining a rule with code and name.""" @@ -141,25 +169,15 @@ def result_nodes( rule_code: str = node["reftarget"] domain = cast("TugboatDomain", env.get_domain("tg")) - data = domain.rules.get(rule_code) - if data and not node.get("explicit"): + entry = domain.rules.get(rule_code.upper()) + + if entry and not node.get("explicit"): node.clear() - node += make_rule_contnode(rule_code, data.rule_name) + node += entry.node() return [node], [] -def make_rule_contnode(rule_code: str, rule_name: str) -> Node: - """Create content node for a rule reference.""" - container = nodes.inline(classes=["inline-rule-ref"]) - container += [ - nodes.inline(text=rule_code, classes=["rule-code"]), - nodes.Text(" "), - nodes.Text(rule_name), - ] - return container - - class TugboatDomain(Domain): name = "tg" @@ -196,7 +214,7 @@ def note_rule(self, rule_code: str, rule_name: str) -> RuleEntry: doc_name=self.env.docname, ) - self.rules[rule_code.upper()] = entry + self.rules[entry.code] = entry # register the label in Sphinx's standard domain std_domain = self.env.domains.standard_domain @@ -204,7 +222,7 @@ def note_rule(self, rule_code: str, rule_name: str) -> RuleEntry: name=entry.ref_name, docname=self.env.docname, node_id=entry.node_id, - title=f"{rule_code} ({strip_rst(rule_name)})", + title=f"{entry.code} ({entry.title_stripped()})", ) return entry @@ -235,20 +253,15 @@ def resolve_xref( target: str, node: pending_xref, contnode: Element, - ) -> Element | None: - if data := self.rules.get(target): + ) -> reference | None: + if entry := self.rules.get(target): return make_refnode( builder, fromdocname, - data.doc_name, - data.node_id, - make_rule_contnode(target, data.title), - data.node_id, + entry.doc_name, + entry.node_id, + entry.node(), + entry.title, ) - return None - -def strip_rst(text: str) -> str: - """Strip reStructuredText markup from the given text.""" - doctree = publish_doctree(text) - return doctree.astext() + return None From f0a34cf618f1d7da5da86856fbb8d57f2f527ae1 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 16:57:47 +0800 Subject: [PATCH 4/4] fix: docstring --- docs/_ext/rule_directive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_ext/rule_directive.py b/docs/_ext/rule_directive.py index b6f8a9d..ce7ad09 100644 --- a/docs/_ext/rule_directive.py +++ b/docs/_ext/rule_directive.py @@ -204,8 +204,8 @@ def note_rule(self, rule_code: str, rule_name: str) -> RuleEntry: Returns ------- - node_id : str - The node ID for the rule target. + entry : RuleEntry + The registered rule entry. """ # register a rule in this domain entry = RuleEntry(