picks up the existing `.highlight pre .k/.n/.kt/.s/.c`
# CSS styling and our `code > a { color: inherit }` rule won't dim the
# link colour.
_HL_PYG_CLASS = {
"keyword": "k", "keywordtype": "kt", "keywordflow": "k",
"preprocessor": "cp", "comment": "c", "comment-multiline": "cm",
"stringliteral": "s", "charliteral": "sc",
}
# Identifier-only token regex; used to linkify `InputArray`,
# `OutputArray`, `Mat`, `Scalar`, `DFT_INVERSE`, … left as plain
# text by Doxygen (no `` on them).
_IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
# `#include "opencv2/…"` / `#include ` regex. Doxygen
# wraps the WHOLE include directive (keyword + quoted/angled path)
# in one `` span — no separate
# token for the path — so the per-identifier linkifier above can't
# see the path. Match the include line and wrap just the path in
# an `` to its local Doxygen file page (`_FILE_URL`).
_INCLUDE_PATH_RE = re.compile(
r'(#include\s*)(["<])([A-Za-z0-9_./+\-]+\.[A-Za-z0-9]+)([">])'
)
def _programlisting(node) -> str:
"""Emit `` as a raw HTML `` block with
per-token `` anchors. The user's spec: code listings must
be CLICKABLE — every recognised identifier (class/typedef/enum
member or ``-marked token) becomes a link to its local
Sphinx target. Falls back to plain text for unknown tokens, so
keywords/numbers/operators stay un-linked but still picked up
by the Pygments-style span classes for colouring."""
from html import escape as _esc
out = ['']
def _linkify_plain(text: str) -> str:
"""Wrap identifier runs that resolve via `_local_url`
with an ``; everything else stays HTML-escaped text."""
parts: list[str] = []
last = 0
for m in _IDENT_RE.finditer(text):
if m.start() > last:
parts.append(_esc(text[last:m.start()]))
tok = m.group(0)
url = _LOCAL_CLASS_URL.get(tok) or _LOCAL_TYPEDEF_URL.get(tok)
if url:
parts.append(
f''
f'{_esc(tok)}')
else:
parts.append(_esc(tok))
last = m.end()
parts.append(_esc(text[last:]))
return "".join(parts)
def _linkify_include_path(escaped: str) -> str:
"""Find `#include "opencv2/…"` patterns in already-escaped
text and wrap the path in an `` to its Doxygen file
page. Operates on POST-escape text (where `"` is `"`
and `<>` are `<`/`>`)."""
def _sub(m: re.Match) -> str:
kw, openq, path, closeq = m.groups()
file_url = _FILE_URL.get(path)
if not file_url:
return m.group(0)
href = f"../../../doc/doxygen/html/{file_url}"
return (f'{kw}{openq}'
f'{path}{closeq}')
# The escape map converts `"` → `"`, `<` → `<`,
# `>` → `>`; rewrite the regex once for that form.
def _re_escaped() -> "re.Pattern":
return re.compile(
r'(#include\s*)("|<)'
r'([A-Za-z0-9_./+\-]+\.[A-Za-z0-9]+)'
r'("|>)'
)
return _re_escaped().sub(_sub, escaped)
def _emit_highlight(hl) -> str:
"""One `` → optionally-wrapped span
sequence preserving inline `` link targets."""
klass = _HL_PYG_CLASS.get(hl.get("class", ""), "")
segs: list[str] = []
if hl.text:
segs.append(_linkify_plain(hl.text))
for child in hl:
if child.tag == "sp":
segs.append(" ")
elif child.tag == "ref":
inner = "".join(child.itertext())
url = _local_url(child.get("refid", ""), inner)
if url:
segs.append(
f''
f'{_esc(inner)}')
else:
segs.append(_linkify_plain(inner))
else:
segs.append(_linkify_plain("".join(child.itertext())))
if child.tail:
segs.append(_linkify_plain(child.tail))
body = "".join(segs)
# Preprocessor lines: linkify any `#include "path"` AFTER
# the per-identifier pass (Doxygen lumps the whole include
# directive into one `class="preprocessor"` span, so the
# path can only be found at the assembled-body level).
if klass == "cp":
body = _linkify_include_path(body)
# Wrap in a Pygments-style `` only for token
# kinds whose colouring would otherwise be lost. The "normal"
# highlight class is the un-coloured default — emit without
# the outer span so embedded `` link colours win.
if klass and body:
return f'{body}'
return body
codelines = node.findall("codeline")
for i, cl in enumerate(codelines):
parts = [_emit_highlight(hl) for hl in cl.findall("highlight")]
line = "".join(parts)
# Empty codeline → emit `` placeholder so the
# blank line doesn't terminate the surrounding raw-HTML
# block under CommonMark rule 7.
out.append(line if line else "")
out.append(' ')
# Wrap the whole thing in a `\n` so MyST parses it as a raw
# HTML block (CommonMark type 6) — the leading line is ``
# which qualifies. Trailing blank line ends the block.
return "\n".join(out) + "\n"
def _ref_link(refid: str, text: str) -> str:
"""Inline `` → blue link to a LOCAL Sphinx target.
The user's spec for cross-references: "blue text, no grey
chip, redirect to local pages, no off-site bounce". So:
- LOCAL URLs only via `_local_url(refid, text)`; when no
local target exists we drop the link (return plain text)
instead of routing the reader to docs.opencv.org.
- emitted as RAW HTML (``)
rather than markdown `[text](url)`. Markdown link syntax
in MyST/Sphinx with a `#fragment` URL is interpreted as a
pending domain xref — when the fragment doesn't match a
registered domain target the result is `text` (unresolved) plus a `#` prefix on the
already-`#` href. Raw HTML bypasses the xref resolver so
the anchor stays a plain in-page link.
- no backticks → no `` grey chip and no 500-weight
on the text.
"""
if not text:
return ""
url = _local_url(refid, text) if refid else None
if not url:
return text
from html import escape as _esc_rl
return (f''
f'{_esc_rl(text)}')
def _local_url(refid: str, name: str) -> str | None:
"""Resolve a Doxygen `` to a Sphinx-local URL.
Layered lookup:
1. Class/struct short-name → curated map (`Mat`, `InputArray`,…)
from `_LOCAL_CLASS_URL`/`_LOCAL_TYPEDEF_URL`.
2. Class/struct member refid (`classcv_1_1Mat_1a`) →
class page + slugified member anchor.
3. Group-anchored function refid (`group__core__array_1ga`)
→ same-page slugified anchor (the function's detail block
emits `({refid})=` which MyST turns into ``).
4. Bare class/struct compound refid → class page.
Returns None when nothing matches — caller renders plain text."""
if not refid:
return None
# Try both the full name and its short form (after the last
# `::`). The typedef/class maps key on the short name only
# (e.g. `InputArray`, not `cv::InputArray`), so a `` whose
# text says `cv::InputArray` would otherwise miss this lookup
# and fall through to the function-slug branch below — minting
# a non-existent `#cv-inputarray` anchor instead of the real
# typedef target `core_basic.html#inputarray`.
short_name = name.rsplit("::", 1)[-1] if name else ""
direct = (_LOCAL_CLASS_URL.get(name) or _LOCAL_TYPEDEF_URL.get(name)
or _LOCAL_CLASS_URL.get(short_name)
or _LOCAL_TYPEDEF_URL.get(short_name))
if direct:
return direct
# Class-member refid (long hex suffix).
cm = re.match(
r"^((?:class|struct)cv_1_1[A-Za-z0-9_]+?)_1"
r"([a-z]{1,3}[0-9a-f]{20,})$", refid)
if cm:
page = cm.group(1)
slug = re.sub(r"_+", "-", refid).lower()
return f"{page}.html#{slug}"
# Group-anchored member on the current group page.
#
# Doxygen uses two refid suffixes for group members:
# * `_1ga` — function in a group → anchor is the
# `_func_slug(name)` (cv-name) emitted by
# `_render_core_basic_func`.
# * `_1gga` — enum VALUE in a group → anchor is the
# C++ v4 id (`_CPPv4N…E`) emitted by the
# per-value `` in the enum
# detail table. The enum-type's parent
# name is captured via `_CV_SYMBOL_URL`
# elsewhere; here we use the value name
# + the parent enum name when available
# to mint the same id.
m = re.search(r"^group__(?P[A-Za-z0-9_]+?)_1(?Pgga|ga)(?P[0-9a-f]+)$", refid)
if m:
kind = m.group("kind")
if kind == "ga": # function
short = (name.rsplit("::", 1)[-1] if name else "")
short = short.split("(", 1)[0].strip()
if not short:
return None
page_stem = m.group("grp").replace("__", "_")
return f"{page_stem}.html#{short.lower()}"
# Enum VALUE `ggaa`: group pages carry no
# per-value anchor, so link to the enum TYPE's doc (refid ga).
hexv = m.group("hex")
if len(hexv) == 65 and hexv[32] == "a":
return _ENUM_REF_URL.get(
f"group__{m.group('grp')}_1ga{hexv[:32]}")
return None
# Bare class/struct compound page.
if refid.startswith(("classcv_1_1", "structcv_1_1")):
return f"{refid}.html"
return None
_formula_md = _render_formula
_BLOCK_TAGS = {"orderedlist", "itemizedlist", "programlisting", "simplesect",
"table", "xrefsect"}
# Map missing 'see' and 'sa' Doxygen sections to MyST 'seealso' admonitions to fix unboxed reference rendering.
_ADMON_BY_KIND = {"note": "note", "warning": "warning",
"attention": "warning", "remark": "note",
"see": "seealso", "sa": "seealso"}
def _emit_block(sub, result: list, level: int) -> None:
t = sub.tag
if t == "programlisting":
result.append(_programlisting(sub))
elif t == "orderedlist":
for i, item in enumerate(sub.findall("listitem"), 1):
result.append(f"{i}. {_listitem_text(item)}")
elif t == "itemizedlist":
for item in sub.findall("listitem"):
result.append(f"- {_listitem_text(item)}")
elif t == "simplesect":
kind = sub.get("kind", "")
admon = _ADMON_BY_KIND.get(kind)
body = "\n\n".join(_blocks(sub, level))
if admon:
result.append(f":::{{{admon}}}\n{body}\n:::")
elif body:
result.append(body)
elif t == "xrefsect":
# @todo/@bug/@deprecated -> a titled admonition box (like Doxygen's).
title = (sub.findtext("xreftitle") or "Note").strip()
desc = sub.find("xrefdescription")
body = "\n\n".join(_blocks(desc, level)) if desc is not None else ""
if body:
result.append(
f":::{{admonition}} {title}\n:class: {title.lower()}\n\n{body}\n:::")
elif t == "table":
rows = sub.findall("row")
if not rows:
return
md_rows = []
for row in rows:
cells = [" ".join(_blocks(e, level)).replace("|", "\\|").replace("\n", " ").strip()
for e in row.findall("entry")]
md_rows.append("| " + " | ".join(cells) + " |")
has_header = (rows[0].find("entry") is not None
and rows[0].find("entry").get("thead") == "yes")
if has_header and md_rows:
cols = len(rows[0].findall("entry"))
table_lines = [md_rows[0], "| " + " | ".join(["----"] * cols) + " |"] + md_rows[1:]
else:
table_lines = md_rows
result.append("\n".join(table_lines))
def _listitem_text(item, indent: int = 0) -> str:
"""Render one 's content, recursively including nested
lists. Without this, sub-bullets (e.g. the Modern-Robotics
link / det(R)=1 properties / Slerp item under the camera-calib
Note) were silently dropped — `_inline` breaks at the first
block tag so the nested `` inside the parent
`` was never recursed into. Doxygen puts the nested list
BOTH inside `` (the common case) AND occasionally as a
direct child of ``, so we check both spots."""
first = ""
extras: list[str] = []
pad = " " * (indent + 2)
def _emit_sublist(nl) -> None:
if nl.tag == "itemizedlist":
for sub in nl.findall("listitem"):
extras.append(
f"{pad}- {_listitem_text(sub, indent + 2)}")
elif nl.tag == "orderedlist":
for i, sub in enumerate(nl.findall("listitem"), 1):
extras.append(
f"{pad}{i}. {_listitem_text(sub, indent + 2)}")
for child in item:
if child.tag == "para":
# Inline text up to (but not including) the first block tag.
txt = _inline(child).strip()
if txt:
if not first:
first = txt
else:
extras.append(pad + txt)
# Nested lists living inside the (Doxygen's
# typical layout for the OpenCV @note bullets).
for inner in child:
if inner.tag in ("itemizedlist", "orderedlist"):
_emit_sublist(inner)
elif child.tag in ("itemizedlist", "orderedlist"):
_emit_sublist(child)
if extras:
return first + "\n" + "\n".join(extras) if first else "\n".join(extras)
return first
def _inline(node) -> str:
parts = []
if node.text:
parts.append(node.text)
for child in node:
t = child.tag
if t in _BLOCK_TAGS:
break
inner = "".join(child.itertext())
if t == "ulink":
url = child.get("url", "")
parts.append(f"[{inner}]({url})" if url else inner)
elif t == "image":
parts.append(_render_image(child))
elif t == "ref":
parts.append(_ref_link(child.get("refid", ""), inner))
elif t == "computeroutput":
parts.append(f"`{inner}`" if inner else "")
elif t == "emphasis":
parts.append(_wrap_emphasis(inner, "*") if inner else "")
elif t in ("bold", "strong"):
parts.append(_wrap_emphasis(inner, "**") if inner else "")
elif t == "formula":
parts.append(_formula_md(inner))
elif t == "sp":
parts.append(" ")
elif t == "linebreak":
parts.append("\n")
else:
parts.append(inner)
if child.tail:
parts.append(child.tail)
return "".join(parts)
def _blocks(node, level: int) -> list[str]:
result = []
children = list(node)
i = 0
while i < len(children):
child = children[i]
t = child.tag
if t == "title":
i += 1
continue
elif t == "simplesect":
# Merge consecutive simplesects of the same kind into one admonition.
kind = child.get("kind", "")
admon = _ADMON_BY_KIND.get(kind)
bodies = []
while (i < len(children) and children[i].tag == "simplesect"
and children[i].get("kind", "") == kind):
bodies.append("\n\n".join(_blocks(children[i], level)))
i += 1
body = "\n\n".join(b for b in bodies if b)
if admon and body:
result.append(f":::{{{admon}}}\n{body}\n:::")
elif body:
result.append(body)
continue
elif t == "para":
pending: list[str] = []
if child.text:
pending.append(child.text)
for sub in child:
if sub.tag in _BLOCK_TAGS:
text = "".join(pending).strip()
if text:
result.append(text)
pending = []
_emit_block(sub, result, level)
if sub.tail and sub.tail.strip():
pending.append(sub.tail)
else:
inner = "".join(sub.itertext())
st = sub.tag
if st == "ulink":
url = sub.get("url", "")
pending.append(f"[{inner}]({url})" if url else inner)
elif st == "image":
pending.append(_render_image(sub))
elif st == "ref":
pending.append(_ref_link(sub.get("refid", ""), inner))
elif st == "computeroutput":
pending.append(f"`{inner}`" if inner else "")
elif st == "emphasis":
pending.append(_wrap_emphasis(inner, "*") if inner else "")
elif st in ("bold", "strong"):
pending.append(_wrap_emphasis(inner, "**") if inner else "")
elif st == "formula":
pending.append(_formula_md(inner))
elif st == "sp":
pending.append(" ")
elif st == "linebreak":
pending.append("\n")
else:
pending.append(inner)
if sub.tail:
pending.append(sub.tail)
text = "".join(pending).strip()
if text:
result.append(text)
elif t in ("sect1", "sect2", "sect3"):
title_text = child.findtext("title") or ""
if title_text:
result.append(f"{'#' * level} {title_text}")
result.extend(_blocks(child, level + 1))
else:
result.extend(_blocks(child, level))
elif t in _BLOCK_TAGS:
_emit_block(child, result, level)
i += 1
return result
# Merge consecutive same-kind admonitions (e.g. two note paras → one box).
raw = [b for b in _blocks(el, h_level) if b.strip()]
merged: list[str] = []
for block in raw:
if merged and block.startswith(":::") and merged[-1].startswith(":::"):
prev_kind = merged[-1].split("\n", 1)[0]
cur_kind = block.split("\n", 1)[0]
if prev_kind == cur_kind:
inner_prev = merged[-1][len(prev_kind)+1:-3].strip()
inner_cur = block[len(cur_kind)+1:-3].strip()
merged[-1] = f"{prev_kind}\n{inner_prev}\n\n{inner_cur}\n:::"
continue
merged.append(block)
return "\n\n".join(merged)
def _normalize_include(path: str) -> str:
p = (path or "").replace("\\", "/").strip()
marker = "/include/"
i = p.rfind(marker)
return p[i + len(marker):] if i >= 0 else p
def _enum_value_desc(ev) -> str:
if ev is None:
return ""
parts = [_doxygen_desc_to_md(ev.find(t)).strip()
for t in ("briefdescription", "detaileddescription")]
return "\n\n".join(p for p in parts if p)
def _md_escape_cell(text: str) -> str:
"""Make `text` safe for a single Markdown table cell."""
return (text or "").replace("\n", " ").replace("\r", " ") \
.replace("|", "\\|").strip()
# memberdef@kind -> per-member breathe directive.
_MEMBER_DIRECTIVE = {
"enum": "doxygenenum",
"function": "doxygenfunction",
"typedef": "doxygentypedef",
"variable": "doxygenvariable",
"define": "doxygendefine",
}
# section title -> detail-block header.
_MEMBER_DETAIL_SECTION = {
"Typedefs": "Typedef Documentation",
"Enumerations": "Enumeration Type Documentation",
"Functions": "Function Documentation",
"Variables": "Variable Documentation",
"Macros": "Macro Definition Documentation",
}
def _sphinx_cpp_v4_id(qualified_name: str) -> str:
"""Sphinx C++ v4 anchor id; mangling must match Sphinx's own."""
parts = qualified_name.split("::")
mangled = "".join(f"{len(p)}{p}" for p in parts)
return f"_CPPv4N{mangled}E"
def _enum_synopsis_html(m: dict, strip_scope: str = "") -> list[str]:
"""Render an enum as an HTML block with clickable names.
Span classes mirror Pygments' cpp-lexer output. `strip_scope` drops the
redundant prefix from displayed names but never from the mangled id."""
import html as _html
qualified = m.get("qualified") or m["name"]
is_strong = bool(m.get("strong"))
keyword = "enum class" if is_strong else "enum"
if is_strong:
prefix = qualified + "::"
elif "::" in qualified:
prefix = qualified.rsplit("::", 1)[0] + "::"
else:
prefix = ""
display_enum = qualified
if strip_scope and qualified.startswith(strip_scope + "::"):
display_enum = qualified[len(strip_scope) + 2:]
display_prefix = prefix
if strip_scope and prefix.startswith(strip_scope + "::"):
display_prefix = prefix[len(strip_scope) + 2:]
enum_id = _sphinx_cpp_v4_id(qualified)
out = [
''
''
]
# href only, no id; anchors live in the detail loop.
enum_link = (
f''
f'{_html.escape(display_enum)}'
)
out.append(
f'{_html.escape(keyword)} '
f'{enum_link} {{'
)
vals = m.get("enum_values", []) or []
for i, v in enumerate(vals):
comma = ',' if i < len(vals) - 1 else ""
val_id = _sphinx_cpp_v4_id(f"{qualified}::{v['name']}")
val_link = (
f''
f'{_html.escape(v["name"])}'
)
init_html = (" " + _html.escape(v["initializer"])) if v["initializer"] else ""
prefix_html = ""
if display_prefix:
stripped = display_prefix.rstrip(":")
prefix_html = (
f'{_html.escape(stripped)}'
f'::'
)
out.append(f" {prefix_html}{val_link}{init_html}{comma}")
out.append('}')
return out
def _enum_synopsis_lines(m: dict, strip_scope: str = "") -> list[str]:
"""Render an enum as a Doxygen-style `enum {…}` code synopsis."""
qualified = m.get("qualified") or m["name"]
is_strong = bool(m.get("strong"))
keyword = "enum class" if is_strong else "enum"
if is_strong:
prefix = qualified + "::"
elif "::" in qualified:
prefix = qualified.rsplit("::", 1)[0] + "::"
else:
prefix = ""
enum_label = qualified
if strip_scope and qualified.startswith(strip_scope + "::"):
enum_label = qualified[len(strip_scope) + 2:]
if strip_scope and prefix.startswith(strip_scope + "::"):
prefix = prefix[len(strip_scope) + 2:]
out = [f"{keyword} {enum_label} {{"]
vals = m.get("enum_values", []) or []
for i, v in enumerate(vals):
comma = "," if i < len(vals) - 1 else ""
init = (" " + v["initializer"]) if v["initializer"] else ""
out.append(f" {prefix}{v['name']}{init}{comma}")
out.append("}")
return out
def _function_signature(member: dict) -> str:
"""`(types)` disambiguator for `{doxygenfunction}`; trailing `const` needed for const methods."""
types = ", ".join((t or "").strip() for t in member.get("param_types", []))
sig = f"({types})"
if member.get("const"):
sig += " const"
return sig
def _class_page_name(refid: str) -> str:
"""Per-class page filename: the Doxygen refid verbatim."""
return refid
def _read_class_data(refid: str, xml_dir: pathlib.Path) -> dict | None:
"""Read a class/struct compound XML for the per-class page."""
import xml.etree.ElementTree as _ET
xml_path = xml_dir / f"{refid}.xml"
if not xml_path.is_file():
return None
try:
root = _ET.parse(xml_path).getroot()
except _ET.ParseError:
return None
cd = root.find("compounddef")
if cd is None:
return None
def _param_type(p) -> str:
t = _itertext(p.find("type"))
arr = (p.findtext("array") or "").strip()
return (t + arr) if arr else t
sections: dict[str, list[dict]] = {}
for sd in cd.findall("sectiondef"):
skind = sd.get("kind", "")
items: list[dict] = []
for md in sd.findall("memberdef"):
mkind = md.get("kind", "")
qualified = (md.findtext("qualifiedname") or "").strip()
name = (md.findtext("name") or "").strip()
enum_values = []
if mkind == "enum":
for ev in md.findall("enumvalue"):
enum_values.append({
"name": (ev.findtext("name") or "").strip(),
"initializer": (ev.findtext("initializer") or "").strip(),
"brief": _enum_value_desc(ev),
})
_dtl, _params, _returns = _member_detail_parts(md)
items.append({
"id": md.get("id", ""),
"kind": mkind,
"name": name,
"qualified": qualified or name,
"type": _itertext(md.find("type")),
"args": (md.findtext("argsstring") or "").strip(),
"param_types": [_param_type(p) for p in md.findall("param")],
"params_sig": [(_param_type(p),
(p.findtext("declname") or "").strip(),
_itertext(p.find("defval")))
for p in md.findall("param")],
"macro_params": ([(p.findtext("defname") or "").strip()
for p in md.findall("param")]
if mkind == "define" else []),
"initializer": _itertext(md.find("initializer")),
"brief": _itertext(md.find("briefdescription")),
"static": md.get("static") == "yes",
"virt": md.get("virt", "non-virtual"),
"const": md.get("const") == "yes",
"explicit": md.get("explicit") == "yes",
"enum_values": enum_values,
"strong": md.get("strong", "no") == "yes",
"template": _member_template(md),
"detailed": _dtl,
"params": _params,
"returns": _returns,
# (text, refid) for every in the return type, param types
# and default values — the exact tokens Doxygen hyperlinks in a
# signature (classes, typedefs, enum values), keyed by refid.
"sig_refs": [((r.text or "").strip(), r.get("refid", ""))
for el in ([md.find("type")]
+ [c for p in md.findall("param")
for c in (p.find("type"),
p.find("defval"))])
if el is not None
for r in el.iter("ref")
if (r.text or "").strip() and r.get("refid")],
})
if items:
sections[skind] = items
# Boolean only; rendering delegated to breathe's :no-members:.
detailed_el = cd.find("detaileddescription")
has_detailed = bool(detailed_el is not None and any(
_itertext(p).strip() for p in detailed_el.findall("para")
))
include = _normalize_include(cd.findtext("includes") or "")
bases = [(b.get("refid"), (b.text or "").strip(), b.get("prot", "public"))
for b in cd.findall("basecompoundref") if b.get("refid")]
return {
"name": (cd.findtext("compoundname") or "").strip(),
"brief": _itertext(cd.find("briefdescription")),
"detailed": has_detailed,
"sections": sections,
"include": include,
"bases": bases,
}
# Filename -> path for every Doxygen `*graph.svg` (coll/call/caller). Built once
# per html_root: a single tree walk replaces an rglob per class/function, which
# matters now that thousands of function detail blocks each look up a graph.
_GRAPH_SVG_INDEX: dict[str, pathlib.Path] | None = None
_GRAPH_SVG_ROOT: pathlib.Path | None = None
def _graph_svg_index(html_root: pathlib.Path) -> dict[str, pathlib.Path]:
global _GRAPH_SVG_INDEX, _GRAPH_SVG_ROOT
if _GRAPH_SVG_INDEX is None or _GRAPH_SVG_ROOT != html_root:
_GRAPH_SVG_ROOT = html_root
_GRAPH_SVG_INDEX = {}
if html_root and html_root.is_dir():
for p in html_root.rglob("*graph.svg"):
_GRAPH_SVG_INDEX.setdefault(p.name, p)
return _GRAPH_SVG_INDEX
def _find_collaboration_svg(refid: str, html_root: pathlib.Path) -> pathlib.Path | None:
"""Locate the legacy Doxygen HTML collaboration SVG for a class."""
return _graph_svg_index(html_root).get(f"{refid}__coll__graph.svg")
def _find_call_graph_svgs(
member: dict,
html_root: pathlib.Path) -> list[tuple[pathlib.Path, str, str]]:
"""Legacy call/caller-graph SVGs for a function member, as (path, intro, alt).
Bridges the member's XML id to the HTML anchor via `_CALL_GRAPH_ANCHORS`
(the SVGs are named after the HTML anchor, not the XML memberdef id)."""
if member.get("kind") != "function":
return []
mid = member.get("id", "")
if "_1" not in mid:
return []
compound = mid.rsplit("_1", 1)[0]
name = member.get("name", "")
if not (compound and name):
return []
anchor = _CALL_GRAPH_ANCHORS.get(
(compound, name, _norm_args(member.get("args", ""))))
if not anchor:
return []
index = _graph_svg_index(html_root)
out: list[tuple[pathlib.Path, str, str]] = []
for suffix, intro, kind in (
("cgraph", "Here is the call graph for this function:", "Call"),
("icgraph", "Here is the caller graph for this function:", "Caller"),
):
svg = index.get(f"{compound}_{anchor}_{suffix}.svg")
if svg is not None:
out.append((svg, intro, f"{kind} graph for {name}"))
return out
def _svg_make_transparent(text: str) -> str:
"""Light-mode: make only the full-canvas backdrop transparent."""
return text.replace('fill="white" stroke="transparent"',
'fill="none" stroke="transparent"', 1)
def _svg_dark_variant(text: str) -> str:
"""Dark-mode: recolour the light Doxygen SVG into a dark diagram."""
import re as _re
text = _svg_make_transparent(text) # backdrop first; order matters
for _old in ('stroke="#666666"', 'stroke="black"', 'stroke="#404040"'):
text = text.replace(_old, 'stroke="#c9d1d9"')
text = text.replace('fill="#666666"', 'fill="#c9d1d9"')
def _process_node(m):
block = m.group(0)
block = block.replace('fill="white"', 'fill="none"')
block = block.replace('fill="grey"', 'fill="none"')
block = block.replace('fill="#999999"', 'fill="#2d333b"')
block = block.replace('fill="#bfbfbf"', 'fill="#2d333b"')
# Non-clickable leaf boxes (std headers) are light grey #e0e0e0 — make
# them the same darker grey as the current-file box, white text below.
block = block.replace('fill="#e0e0e0"', 'fill="#2d333b"')
block = block.replace(']*class="node"[^>]*>.*?',
_process_node, text, flags=_re.DOTALL)
text = text.replace('fill="grey"', 'fill="none"')
text = text.replace('fill="#999999"', 'fill="#2d333b"')
text = text.replace('fill="#bfbfbf"', 'fill="#2d333b"')
text = text.replace('fill="#e0e0e0"', 'fill="#2d333b"')
# Lookahead avoids double-prefixing per-node texts.
text = _re.sub(r' bool:
import xml.etree.ElementTree as _ET
changed = False
for md in cd.iter("memberdef"):
if md.get("kind") != "function":
continue
de = md.find("detaileddescription")
if de is None:
continue
# param name -> declared type, to spot callback / function-pointer params.
ptypes: dict = {}
for p in md.findall("param"):
dn = (p.findtext("declname") or "").strip()
te = p.find("type")
if dn:
ptypes[dn] = "".join(te.itertext()) if te is not None else ""
hoisted: list = []
for it in de.iter("parameteritem"):
pd = it.find("parameterdescription")
para = pd.find("para") if pd is not None else None
if para is None:
continue
ptype = " ".join(ptypes.get("".join(n.itertext()).strip(), "")
for n in it.findall(".//parametername"))
if "Callback" in ptype or "(*" in ptype or "function<" in ptype:
continue
text = para.text or ""
m = _SPILLED_PROSE_RE.search(text)
if m is None or m.start() == 0:
continue
keep = text[:m.start()].rstrip()
if not keep or re.search(r"\b(?:function|callback)\b", keep, re.I):
continue
spill = _ET.Element("para")
spill.text = text[m.start():]
for child in list(para): # all children follow the leading text
para.remove(child)
spill.append(child)
para.text = keep
hoisted.append(spill)
changed = True
if hoisted:
# Insert ahead of the carrying the , so the
# prose reads as body text before the parameter table.
kids = list(de)
at = next((i for i, p in enumerate(kids)
if p.find("parameterlist") is not None), len(kids))
for off, sp in enumerate(hoisted):
de.insert(at + off, sp)
return changed
def _patch_namespace_xml_for_breathe(xml_dir: pathlib.Path,
out_dir: pathlib.Path) -> None:
"""Mirror `xml_dir` symlinks into `out_dir`, inlining group-only memberdefs.
breathe's by-name lookup ignores bare ``s, so @addtogroup
members must be copied into the compound's sectiondef for it to resolve."""
import xml.etree.ElementTree as _ET
import os as _osmod, shutil as _shutil
src_index = xml_dir / "index.xml"
# Real stamp file, not dst_index (a symlink stat() would follow).
stamp = out_dir / ".mirror_complete"
if (src_index.is_file() and stamp.is_file()
and stamp.stat().st_mtime >= src_index.stat().st_mtime):
return
out_dir.mkdir(parents=True, exist_ok=True)
# 1) Mirror xml_dir into out_dir as symlinks.
for child in list(out_dir.iterdir()):
if child.is_symlink() or child.is_file():
child.unlink()
elif child.is_dir():
_shutil.rmtree(child)
for src in xml_dir.iterdir():
dst = out_dir / src.name
try:
_osmod.symlink(src, dst)
except (OSError, NotImplementedError):
_shutil.copy2(src, dst)
# 2) Cache for parsed group XMLs.
_group_cache: dict[str, _ET.ElementTree] = {}
def _load_group(group_id: str):
if group_id in _group_cache:
return _group_cache[group_id]
gx = xml_dir / f"{group_id}.xml"
if not gx.is_file():
_group_cache[group_id] = None
return None
try:
_group_cache[group_id] = _ET.parse(gx)
except _ET.ParseError:
_group_cache[group_id] = None
return _group_cache[group_id]
# 3) Patch group-memberdef ``s in each non-group compound.
_SKIP_PREFIXES = ("group", "index")
for compound_file in xml_dir.glob("*.xml"):
if any(compound_file.name.startswith(p) for p in _SKIP_PREFIXES):
continue
try:
tree = _ET.parse(compound_file)
except _ET.ParseError:
continue
root = tree.getroot()
cd = root.find("compounddef")
if cd is None:
continue
existing_ids = {md.get("id") for md in cd.iter("memberdef")}
patched = False
for sd in cd.findall("sectiondef"):
for member in list(sd.findall("member")):
refid = member.get("refid", "")
if not refid or refid in existing_ids:
continue
# Compound id is the refid before "_1".
if not refid.startswith("group__"):
continue
sep = refid.find("_1")
if sep < 0:
continue
group_id = refid[:sep]
gtree = _load_group(group_id)
if gtree is None:
continue
for md in gtree.getroot().iter("memberdef"):
if md.get("id") == refid:
sd.append(md)
existing_ids.add(refid)
patched = True
break
if patched:
out_file = out_dir / compound_file.name
if out_file.is_symlink() or out_file.is_file():
out_file.unlink()
tree.write(out_file, encoding="utf-8", xml_declaration=True)
# 3b) Hoist function-level prose that ran on into a @param description back
# into the function body. Runs over every mirrored compound (groups
# included) so the fix shows wherever breathe resolves the function.
for compound_file in list(out_dir.glob("*.xml")):
if compound_file.name == "index.xml":
continue
try:
tree = _ET.parse(compound_file)
except _ET.ParseError:
continue
cd = tree.getroot().find("compounddef")
if cd is None:
continue
if _hoist_spilled_param_prose(cd):
if compound_file.is_symlink() or compound_file.is_file():
compound_file.unlink()
tree.write(compound_file, encoding="utf-8", xml_declaration=True)
# 4) Record completion.
stamp.touch()
# -- Namespace pages ---------------------------------------------------------
# Doxygen 1.12 namespace XML lacks ; these readers bridge it.
def _build_ns_group_map(all_refids: list[str],
xml_dir: pathlib.Path) -> dict[str, set]:
"""Return ``namespace_name -> set(group compound-name)``."""
import xml.etree.ElementTree as _ET
ns_to_groups: dict[str, set] = {}
for refid in all_refids:
xml_path = xml_dir / f"{refid}.xml"
if not xml_path.is_file():
continue
try:
cd = _ET.parse(xml_path).getroot().find("compounddef")
except _ET.ParseError:
continue
if cd is None:
continue
cname = (cd.findtext("compoundname") or "").strip()
for inn in cd.findall("innernamespace"):
ns_xml = xml_dir / f"{inn.get('refid', '')}.xml"
if not ns_xml.is_file():
continue
try:
ns_cd = _ET.parse(ns_xml).getroot().find("compounddef")
except _ET.ParseError:
continue
if ns_cd is None:
continue
ns_name = (ns_cd.findtext("compoundname") or "").strip()
if not ns_name:
continue
if not (ns_cd.findall("sectiondef") or ns_cd.findall("innerclass")
or ns_cd.findall("innernamespace")):
continue
ns_to_groups.setdefault(ns_name, set()).add(cname)
return ns_to_groups
def _namespaces_for_group(group_name: str, xml_dir: pathlib.Path,
ns_group_map: dict[str, set]) -> list[dict]:
"""Return ``{name, refid, brief, detailed}`` for ``group_name``'s namespaces."""
import xml.etree.ElementTree as _ET, glob as _glob
wanted = {ns for ns, grps in ns_group_map.items() if group_name in grps}
out: list[dict] = []
for ns_name in sorted(wanted):
ns_file = xml_dir / ("namespace" + "_1_1".join(ns_name.split("::")) + ".xml")
if not ns_file.is_file():
for f in _glob.glob(str(xml_dir / "namespacecv*.xml")):
try:
cd = _ET.parse(f).getroot().find("compounddef")
except _ET.ParseError:
continue
if cd is not None and (cd.findtext("compoundname") or "").strip() == ns_name:
ns_file = pathlib.Path(f)
break
if not ns_file.is_file():
continue
try:
cd = _ET.parse(ns_file).getroot().find("compounddef")
except _ET.ParseError:
continue
if cd is None:
continue
brief = _itertext(cd.find("briefdescription"))
detailed = _doxygen_desc_to_md(cd.find("detaileddescription"))
out.append({"name": ns_name, "refid": cd.get("id", ""),
"brief": brief, "detailed": detailed})
return out
def _read_namespace_member_sections(ns_refid: str,
patched_xml_dir: pathlib.Path) -> dict:
"""Member sections for a namespace from the PATCHED XML."""
import xml.etree.ElementTree as _ET
if not ns_refid:
return {}
xml_path = patched_xml_dir / f"{ns_refid}.xml"
if not xml_path.is_file():
return {}
try:
cd = _ET.parse(xml_path).getroot().find("compounddef")
except _ET.ParseError:
return {}
return _parse_member_sections(cd) if cd is not None else {}
def _namespace_innerclasses(ns_name: str, xml_dir: pathlib.Path) -> list[tuple]:
"""Return ``(refid, qualified_name, kind, brief)`` for classes directly in ``ns_name``."""
import xml.etree.ElementTree as _ET
ns_prefix = ns_name + "::"
refid_prefix = ns_name.replace("::", "_1_1") + "_1_1"
out: list[tuple] = []
for kind in ("struct", "class"):
for xml_file in sorted(xml_dir.glob(f"{kind}{refid_prefix}*.xml")):
try:
cd = _ET.parse(xml_file).getroot().find("compounddef")
except _ET.ParseError:
continue
if cd is None:
continue
cname = (cd.findtext("compoundname") or "").strip()
if "::" in cname[len(ns_prefix):]: # in a sub-namespace
continue
out.append((xml_file.stem, cname, kind,
_itertext(cd.find("briefdescription"))))
return out
__all__ = [
"_itertext", "_type_to_md", "_doxygen_desc_to_md",
"_enum_value_desc", "_normalize_include",
"_MEMBERDEF_SECTIONS", "_read_class_brief",
"_build_api_hierarchy", "_parse_member_sections", "_md_escape_cell",
"_MEMBER_DIRECTIVE", "_MEMBER_DETAIL_SECTION", "_sphinx_cpp_v4_id",
"_enum_synopsis_html", "_enum_synopsis_lines", "_function_signature",
"_class_page_name", "_read_class_data", "_find_collaboration_svg",
"_find_call_graph_svgs",
"_svg_make_transparent", "_svg_dark_variant",
"_patch_namespace_xml_for_breathe",
"_build_ns_group_map", "_namespaces_for_group",
"_read_namespace_member_sections", "_namespace_innerclasses",
]