1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-22 03:43:03 +04:00
Files
omrope79 04aee009aa Merge pull request #29220 from omrope79:doc_optimizations_v4
[FOLLOW UP] : Documentation optimizations for the new Sphinx structure #29220

### Pull Request Readiness Checklist

This PR serves as a follow-up to the new documentation system introduced in [#29206](https://github.com/opencv/opencv/pull/29206)
Co-authored by: @abhishek-gola @kirtijindal14 @Akansha-977 @Prasadayus @varun-jaiswal17

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
2026-06-05 14:18:27 +03:00

1424 lines
60 KiB
Python

# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Doxygen XML -> Markdown primitives for the API-reference stubs."""
from __future__ import annotations
import copy as _copy
import pathlib, re, os as _os, shutil as _shutil, textwrap as _textwrap
from .state import *
def _wrap_emphasis(inner: str, delim: str) -> str:
"""Wrap `inner` in a `*`/`**` run, hoisting any leading/trailing whitespace
outside the markers. CommonMark needs the delimiters to hug the text
(`**x**`, not `** x **`), but Doxygen keeps the spaces inside `<b> … </b>`."""
stripped = inner.strip()
if not stripped:
return inner
lead = inner[:len(inner) - len(inner.lstrip())]
trail = inner[len(inner.rstrip()):]
return f"{lead}{delim}{stripped}{delim}{trail}"
_AMS_BLOCK_ENVS = frozenset((
"align", "align*", "alignat", "alignat*", "flalign", "flalign*",
"gather", "gather*", "multline", "multline*",
"equation", "equation*", "eqnarray", "eqnarray*",
))
def _render_formula(raw: str) -> str:
s = (raw or "").strip()
if s.startswith("\\[") and s.endswith("\\]"):
inner = s[2:-2].strip()
m = re.match(r"^\\begin\{([a-zA-Z]+\*?)\}", inner)
if (m and m.group(1) in _AMS_BLOCK_ENVS
and inner.rstrip().endswith("\\end{%s}" % m.group(1))):
return f"\n\n{inner}\n\n"
return f"\n\n$$\n{inner}\n$$\n\n"
return s
def _render_image(node) -> str:
if node.get("type") != "html":
return ""
name = (node.get("name") or "").strip()
caption = "".join(node.itertext()).strip()
hit = _IMAGE_INDEX.get(name)
if not hit:
return ""
return f"![{caption}](/{hit})"
def _itertext(el) -> str:
if el is None:
return ""
parts: list[str] = []
def _walk(node) -> None:
if node.tag == "formula":
parts.append(_render_formula(node.text or ""))
elif node.tag == "image":
parts.append(_render_image(node)) # caption text not recursed
else:
if node.text:
parts.append(node.text)
for child in node:
_walk(child)
if child.tail:
parts.append(child.tail)
_walk(el)
return "".join(parts).strip()
# memberdef@kind -> section title; order mirrors Doxygen group page.
_MEMBERDEF_SECTIONS = (
("typedef", "Typedefs"),
("enum", "Enumerations"),
("function", "Functions"),
("variable", "Variables"),
("define", "Macros"),
)
def _read_class_brief(refid: str, xml_dir: pathlib.Path,
_cache: dict = {}) -> str:
"""Read brief description from a class/struct compound XML; cached."""
if refid in _cache:
return _cache[refid]
import xml.etree.ElementTree as _ET
xml_path = xml_dir / f"{refid}.xml"
brief = ""
if xml_path.is_file():
try:
ccd = _ET.parse(xml_path).getroot().find("compounddef")
if ccd is not None:
brief = _itertext(ccd.find("briefdescription"))
except _ET.ParseError:
pass
_cache[refid] = brief
return brief
def _member_template(md) -> str:
"""`template<...>` prefix for a memberdef."""
tpl = md.find("templateparamlist")
if tpl is None:
return ""
parts = []
for p in tpl.findall("param"):
t = _itertext(p.find("type"))
d = (p.findtext("declname") or "").strip()
parts.append(f"{t} {d}".strip() if d else t)
return f"template<{', '.join(parts)}>" if parts else ""
def _member_detail_parts(md):
de = md.find("detaileddescription")
if de is None:
return "", [], ""
params, returns = [], ""
for para in de.findall("para"):
for pl in para.findall("parameterlist"):
if pl.get("kind") in ("param", "templateparam"):
for it in pl.findall("parameteritem"):
nm = ", ".join(
t for t in (_itertext(n) for n in
it.findall(".//parametername")) if t)
# Block-aware: a description carrying an <itemizedlist>
# (e.g. calibration `flags`) keeps its bullets as real
# Markdown instead of collapsing into a run-on paragraph.
d = _doxygen_desc_to_md(it.find("parameterdescription"))
if nm:
params.append((nm, d))
for ss in para.findall("simplesect"):
if ss.get("kind") == "return":
returns = _itertext(ss)
# Prune the param/return chrome (rendered separately) then convert the rest
# with full block support so lists and notes survive. Preserve tail text when
# removing elements so descriptions after parameterlist are not lost.
pruned = _copy.deepcopy(de)
for para in pruned.findall("para"):
for child in list(para):
if child.tag == "parameterlist":
# Preserve tail text (text after the element) by moving it to previous sibling
if child.tail:
prev_idx = list(para).index(child) - 1
if prev_idx >= 0:
prev_sibling = para[prev_idx]
prev_sibling.tail = (prev_sibling.tail or "") + child.tail
else:
# No previous sibling, prepend to para text
para.text = (para.text or "") + child.tail
para.remove(child)
elif (child.tag == "simplesect"
and child.get("kind") in ("param", "templateparam", "return")):
# Same tail preservation for simplesect
if child.tail:
prev_idx = list(para).index(child) - 1
if prev_idx >= 0:
prev_sibling = para[prev_idx]
prev_sibling.tail = (prev_sibling.tail or "") + child.tail
else:
para.text = (para.text or "") + child.tail
para.remove(child)
return _doxygen_desc_to_md(pruned), params, returns
# I/O proxy types as a variable type = Doxygen phantom; drop it.
_PHANTOM_VAR_TYPE_RE = re.compile(
r"\b(?:Input|Output|InputOutput)Array(?:OfArrays)?\b")
def _is_phantom_variable(member: dict) -> bool:
return (member.get("kind") == "variable"
and bool(_PHANTOM_VAR_TYPE_RE.search(member.get("type") or "")))
def _parse_member_sections(cd) -> dict[str, list[dict]]:
"""Parse compounddef memberdefs into `{section_title: [member dict]}`."""
sections: dict[str, list[dict]] = {}
_seen_member_ids: set[str] = set()
for sd in cd.findall("sectiondef"):
# Doxygen `@name` member-group title; carried onto each member.
sd_header = (sd.findtext("header") or "").strip()
for md in sd.findall("memberdef"):
kind = md.get("kind", "")
section_title = dict(_MEMBERDEF_SECTIONS).get(kind)
if not section_title:
continue
_mid = md.get("id", "")
if _mid and _mid in _seen_member_ids:
continue
_seen_member_ids.add(_mid)
qualified = (md.findtext("qualifiedname") or "").strip()
if not qualified:
qualified = (md.findtext("name") or "").strip()
# Merge <array> suffix into <type> or breathe drops it.
def _param_type(p) -> str:
t = _itertext(p.find("type"))
arr = (p.findtext("array") or "").strip()
return (t + arr) if arr else t
param_types = [_param_type(p) for p in md.findall("param")]
# (type, name, default) per parameter — drives the multi-line,
# column-aligned function signature in the detail card.
def _param_sig(p) -> tuple:
return (_param_type(p),
(p.findtext("declname") or "").strip(),
_itertext(p.find("defval")))
params_sig = [_param_sig(p) for p in md.findall("param")]
# Function-like macro params carry only a <defname> (no type/declname).
macro_params = ([(p.findtext("defname") or "").strip()
for p in md.findall("param")] if kind == "define"
else [])
# `= value` for a variable; the macro body for a define.
initializer = _itertext(md.find("initializer"))
enum_values = []
is_strong = md.get("strong", "no") == "yes"
if kind == "enum":
for ev in md.findall("enumvalue"):
enum_values.append({
"name": (ev.findtext("name") or "").strip(),
"initializer": (ev.findtext("initializer") or "").strip(),
# Per-enumerator brief for the detail block.
"brief": _enum_value_desc(ev),
})
_dtl, _params, _returns = _member_detail_parts(md)
_loc = md.find("location")
_include_file = _normalize_include(
(_loc.get("file") if _loc is not None else "") or "")
member = {
"id": md.get("id", ""),
"kind": kind,
"name": (md.findtext("name") or "").strip(),
"qualified": qualified,
"definition": (md.findtext("definition") or "").strip(),
"virt": md.get("virt", "non-virtual"),
"type": _itertext(md.find("type")),
"args": (md.findtext("argsstring") or "").strip(),
"param_types": param_types,
"params_sig": params_sig,
"macro_params": macro_params,
"initializer": initializer,
"brief": _itertext(md.find("briefdescription")),
"enum_values": enum_values,
"strong": is_strong,
"static": md.get("static") == "yes",
"inline": md.get("inline") == "yes",
"const": md.get("const") == "yes",
"template": _member_template(md),
"include_file": _include_file,
"section_header": sd_header,
"detailed": _dtl,
"params": _params,
"returns": _returns,
}
if _is_phantom_variable(member):
continue
sections.setdefault(section_title, []).append(member)
return sections
def _build_api_hierarchy(refid: str, xml_dir: pathlib.Path,
_seen: set | None = None) -> dict | None:
"""Walk a group XML's <innergroup> children recursively."""
import xml.etree.ElementTree as _ET
_seen = _seen if _seen is not None else set()
if refid in _seen:
return None
_seen.add(refid)
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
name = (cd.findtext("compoundname") or "").strip()
title = (cd.findtext("title") or name).strip()
brief_el = cd.find("briefdescription")
brief = _doxygen_desc_to_md(brief_el).strip() if brief_el is not None else ""
detailed_el = cd.find("detaileddescription")
detailed = _doxygen_desc_to_md(detailed_el) if detailed_el is not None else ""
# Inner classes (public only). One read per class's XML for its brief.
# `qualified` is what `{doxygenclass}` needs (e.g. cv::ocl::Context); the
# innerclass element's text already carries that, but normalize spaces.
innerclasses = []
for ic in cd.findall("innerclass"):
if ic.get("prot") != "public":
continue
ic_refid = ic.get("refid", "")
qualified = " ".join((ic.text or "").split())
innerclasses.append({
"refid": ic_refid,
"name": qualified,
"qualified": qualified,
"kind": "struct" if ic_refid.startswith("struct") else "class",
"brief": _read_class_brief(ic_refid, xml_dir),
})
sections = _parse_member_sections(cd)
children = []
for ig in cd.findall("innergroup"):
child = _build_api_hierarchy(ig.get("refid"), xml_dir, _seen)
if child is not None:
children.append(child)
return {"name": name, "title": title, "brief": brief, "detailed": detailed,
"innerclasses": innerclasses, "sections": sections,
"children": children}
def _type_to_md(type_elem) -> str:
"""Render <type> XML as Markdown; turns <ref> children into links."""
if type_elem is None:
return ""
out = type_elem.text or ""
for child in type_elem:
if child.tag == "ref":
word = (child.text or "").strip()
refid = child.get("refid", "")
kindref = child.get("kindref", "")
if kindref == "compound" and refid in _ANCHOR_TO_DOC:
fn = _ANCHOR_TO_DOC[refid].split("/")[-1]
out += f"[{word}]({fn}.md)"
elif refid:
out += f"[{word}](#{refid})"
else:
out += word
out += child.tail or ""
return out.strip()
def _doxygen_desc_to_md(el, h_level: int = 3) -> str:
"""Convert a Doxygen <detaileddescription> element to Markdown."""
if el is None:
return ""
def _hl_text(hl_node) -> str:
parts = []
if hl_node.text:
parts.append(hl_node.text)
for child in hl_node:
if child.tag == "sp":
parts.append(" ")
else:
parts.append("".join(child.itertext()))
if child.tail:
parts.append(child.tail)
return "".join(parts)
# Doxygen <highlight class="…"> → Pygments token class, so the
# rendered <pre> 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 `<ref>` on them).
_IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
# `#include "opencv2/…"` / `#include <opencv2/…>` regex. Doxygen
# wraps the WHOLE include directive (keyword + quoted/angled path)
# in one `<highlight class="preprocessor">` 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 `<a>` 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 `<programlisting>` as a raw HTML `<pre>` block with
per-token `<a>` anchors. The user's spec: code listings must
be CLICKABLE — every recognised identifier (class/typedef/enum
member or `<ref>`-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 = ['<div class="highlight-cpp notranslate"><div class="highlight"><pre>']
def _linkify_plain(text: str) -> str:
"""Wrap identifier runs that resolve via `_local_url`
with an `<a>`; 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'<a class="reference internal" href="{url}">'
f'<span class="n">{_esc(tok)}</span></a>')
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 `<a>` to its Doxygen file
page. Operates on POST-escape text (where `"` is `&quot;`
and `<>` are `&lt;`/`&gt;`)."""
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'<a class="reference external opencv-include-link" '
f'href="{href}">{path}</a>{closeq}')
# The escape map converts `"` → `&quot;`, `<` → `&lt;`,
# `>` → `&gt;`; rewrite the regex once for that form.
def _re_escaped() -> "re.Pattern":
return re.compile(
r'(#include\s*)(&quot;|&lt;)'
r'([A-Za-z0-9_./+\-]+\.[A-Za-z0-9]+)'
r'(&quot;|&gt;)'
)
return _re_escaped().sub(_sub, escaped)
def _emit_highlight(hl) -> str:
"""One `<highlight class="…">` → optionally-wrapped span
sequence preserving inline `<ref>` 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'<a class="reference internal" href="{url}">'
f'<span class="n">{_esc(inner)}</span></a>')
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 `<span class="X">` 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 `<a>` link colours win.
if klass and body:
return f'<span class="{klass}">{body}</span>'
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 `<span></span>` placeholder so the
# blank line doesn't terminate the surrounding raw-HTML
# block under CommonMark rule 7.
out.append(line if line else "<span></span>")
out.append('</pre></div></div>')
# Wrap the whole thing in a `\n` so MyST parses it as a raw
# HTML block (CommonMark type 6) — the leading line is `<div…>`
# which qualifies. Trailing blank line ends the block.
return "\n".join(out) + "\n"
def _ref_link(refid: str, text: str) -> str:
"""Inline `<ref>` → 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 (`<a class="reference internal" …>`)
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 `<span class="xref
myst">text</span>` (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 `<code>` 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'<a class="reference internal" href="{url}">'
f'{_esc_rl(text)}</a>')
def _local_url(refid: str, name: str) -> str | None:
"""Resolve a Doxygen `<ref>` 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<hex>`) →
class page + slugified member anchor.
3. Group-anchored function refid (`group__core__array_1ga<hex>`)
→ same-page slugified anchor (the function's detail block
emits `({refid})=` which MyST turns into `<span id="…">`).
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 `<ref>` 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<hex>` — function in a group → anchor is the
# `_func_slug(name)` (cv-name) emitted by
# `_render_core_basic_func`.
# * `_1gga<hex>` — enum VALUE in a group → anchor is the
# C++ v4 id (`_CPPv4N…E`) emitted by the
# per-value `<span id>` 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<grp>[A-Za-z0-9_]+?)_1(?P<kind>gga|ga)(?P<hex>[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 `gga<enumhash>a<valuehash>`: group pages carry no
# per-value anchor, so link to the enum TYPE's doc (refid ga<enumhash>).
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 <listitem>'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 `<itemizedlist>` inside the parent
`<para>` was never recursed into. Doxygen puts the nested list
BOTH inside `<para>` (the common case) AND occasionally as a
direct child of `<listitem>`, 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 <para> (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 = [
'<div class="highlight-cpp notranslate opencv-enum-synopsis">'
'<div class="highlight"><pre>'
]
# href only, no id; anchors live in the detail loop.
enum_link = (
f'<a class="opencv-enum-link" href="#{enum_id}">'
f'<span class="n">{_html.escape(display_enum)}</span></a>'
)
out.append(
f'<span class="k">{_html.escape(keyword)}</span> '
f'{enum_link} <span class="p">{{</span>'
)
vals = m.get("enum_values", []) or []
for i, v in enumerate(vals):
comma = '<span class="p">,</span>' if i < len(vals) - 1 else ""
val_id = _sphinx_cpp_v4_id(f"{qualified}::{v['name']}")
val_link = (
f'<a class="opencv-enum-link" href="#{val_id}">'
f'<span class="n">{_html.escape(v["name"])}</span></a>'
)
init_html = (" " + _html.escape(v["initializer"])) if v["initializer"] else ""
prefix_html = ""
if display_prefix:
stripped = display_prefix.rstrip(":")
prefix_html = (
f'<span class="n">{_html.escape(stripped)}</span>'
f'<span class="o">::</span>'
)
out.append(f" {prefix_html}{val_link}{init_html}{comma}")
out.append('<span class="p">}</span></pre></div></div>')
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 <ref> 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('<text ', '<text fill="#ffffff" ')
return block
text = _re.sub(r'<g[^>]*class="node"[^>]*>.*?</g>',
_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'<text (?!fill)', '<text fill="#ffffff" ', text)
return text
_SPILLED_PROSE_RE = re.compile(r"\b(?:This|The)\s+(?:function|method)\b")
def _hoist_spilled_param_prose(cd) -> 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 <para> carrying the <parameterlist>, 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 `<member refid>`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 `<member refid>`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 <innerclass>; 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",
]