vendor: OpenCV 5.0.0 snapshot at 40738fb16ceddb5fb3fea747585f7ce6abb0605b
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
"""Doc-build engine for the OpenCV Sphinx wrapper.
|
||||
|
||||
conf.py stays a thin Sphinx-settings file; everything heavy lives here:
|
||||
|
||||
* ``state`` — shared config, paths, tag maps, bib/citation numbering,
|
||||
redirect map, anchor indexes, constants
|
||||
* ``xml_render`` — Doxygen XML -> Markdown primitives (incl. enum synopsis)
|
||||
* ``stubs`` — API-reference stub writers (groups / classes)
|
||||
* ``translate`` — Doxygen-flavored .markdown -> MyST (the source-read engine)
|
||||
* ``patches`` — Sphinx C++ domain / breathe warning patches
|
||||
* ``postprocess`` — build-finished hook that inlines collaboration-diagram SVGs
|
||||
* ``build`` — import-time orchestration that populates the shared indexes
|
||||
"""
|
||||
@@ -0,0 +1,447 @@
|
||||
# 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.
|
||||
|
||||
"""Import-time orchestration: populate the shared indexes."""
|
||||
from __future__ import annotations
|
||||
import pathlib, re, os as _os, shutil as _shutil, textwrap as _textwrap
|
||||
from .state import *
|
||||
from .xml_render import _patch_namespace_xml_for_breathe
|
||||
from .stubs import _generate_api_stubs
|
||||
|
||||
|
||||
def _discover_orphan_groups(xml_dir):
|
||||
if not xml_dir.is_dir():
|
||||
return [], []
|
||||
folders = set()
|
||||
for _root in (OPENCV_ROOT / "modules", CONTRIB_ROOT):
|
||||
if _root.is_dir():
|
||||
folders.update(d.name for d in _root.iterdir()
|
||||
if (d / "include" / "opencv2").is_dir())
|
||||
skip = set(folders) | {f.replace("_", "__") for f in folders}
|
||||
skip |= {_module_group_stem(m) for m in folders}
|
||||
all_groups, child = set(), set()
|
||||
for gx in xml_dir.glob("group__*.xml"):
|
||||
all_groups.add(gx.stem)
|
||||
try:
|
||||
xml = gx.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
child.update(re.findall(r'<innergroup refid="(group__[^"]+)"', xml))
|
||||
main, extra = [], []
|
||||
for g in sorted(all_groups - child):
|
||||
stem = g[len("group__"):]
|
||||
name = stem.replace("__", "_")
|
||||
if stem in skip or name in skip:
|
||||
continue
|
||||
xml = (xml_dir / f"{g}.xml").read_text(encoding="utf-8", errors="ignore")
|
||||
loc = re.search(r'<location file="([^"]*)"', xml)
|
||||
(extra if loc and "opencv_contrib/" in loc.group(1) else main).append(name)
|
||||
return main, extra
|
||||
|
||||
|
||||
# Skip when input root is DOC_ROOT: writing there is forbidden.
|
||||
if _BIB_ENTRIES_SORTED and SPHINX_INPUT_ROOT != DOC_ROOT:
|
||||
try:
|
||||
SPHINX_INPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
(SPHINX_INPUT_ROOT / "citelist.markdown").write_text(
|
||||
_bib_render_all(_BIB_ENTRIES_SORTED, _CITE_NUMBER),
|
||||
encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Internal scan: enabled subtrees + standalone pages.
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "tutorials" / "tutorials.markdown")
|
||||
for _m in DOC_MODULES:
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "tutorials" / _m)
|
||||
if JS_DOC_MODULES:
|
||||
_scan_internal(DOC_ROOT / "js_tutorials" / "js_tutorials.markdown",
|
||||
base=DOC_ROOT)
|
||||
for _m in JS_DOC_MODULES:
|
||||
_scan_internal(DOC_ROOT / "js_tutorials" / _m, base=DOC_ROOT)
|
||||
if PY_DOC_MODULES:
|
||||
_scan_internal(DOC_ROOT / "py_tutorials" / "py_tutorials.markdown",
|
||||
base=DOC_ROOT)
|
||||
for _m in PY_DOC_MODULES:
|
||||
_scan_internal(DOC_ROOT / "py_tutorials" / _m, base=DOC_ROOT)
|
||||
|
||||
_contrib_dir = SPHINX_INPUT_ROOT / "tutorials_contrib"
|
||||
_contrib_root_md = next(
|
||||
(p for p in (_contrib_dir / "contrib_root.markdown",
|
||||
_contrib_dir / "tutorials_contrib.markdown") if p.is_file()),
|
||||
_contrib_dir / "contrib_root.markdown")
|
||||
if _contrib_root_md.is_file():
|
||||
_scan_internal(_contrib_root_md)
|
||||
for _m in CONTRIB_MODULES:
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "tutorials_contrib" / _m)
|
||||
# Standalone top-level pages.
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "faq.markdown")
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "citelist.markdown")
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "intro.markdown")
|
||||
|
||||
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".webp"}
|
||||
for _root in ((DOC_ROOT / "tutorials").rglob("images/*"),
|
||||
(DOC_ROOT / "js_tutorials").rglob("images/*"),
|
||||
(DOC_ROOT / "js_tutorials" / "js_assets").glob("*"),
|
||||
(DOC_ROOT / "py_tutorials").rglob("images/*"),
|
||||
(DOC_ROOT / "images").glob("*")):
|
||||
for _img in _root:
|
||||
if _img.is_file():
|
||||
_IMAGE_INDEX.setdefault(_img.name, _img.relative_to(DOC_ROOT).as_posix())
|
||||
for _m in CONTRIB_MODULES:
|
||||
# <m>/tutorials/**/images/*
|
||||
_tut = CONTRIB_ROOT / _m / "tutorials"
|
||||
if _tut.is_dir():
|
||||
for _img in _tut.rglob("images/*"):
|
||||
if _img.is_file():
|
||||
_rel = _img.relative_to(_tut).as_posix()
|
||||
_IMAGE_INDEX.setdefault(_img.name,
|
||||
f"tutorials_contrib/{_m}/{_rel}")
|
||||
# Contrib images outside <m>/tutorials/.
|
||||
for _sub in ("doc", "samples"):
|
||||
_src = CONTRIB_ROOT / _m / _sub
|
||||
if _src.is_dir():
|
||||
for _img in _src.rglob("*"):
|
||||
if _img.is_file() and _img.suffix.lower() in _IMAGE_EXTS:
|
||||
_rel = _img.relative_to(CONTRIB_ROOT).as_posix()
|
||||
_IMAGE_INDEX.setdefault(_img.name,
|
||||
f"contrib_modules/{_rel}")
|
||||
|
||||
if API_MODULES:
|
||||
_api_pics = SPHINX_INPUT_ROOT / "api_pics"
|
||||
_stage_pics = SPHINX_INPUT_ROOT != DOC_ROOT
|
||||
if _stage_pics:
|
||||
_api_pics.mkdir(parents=True, exist_ok=True)
|
||||
_modules_root = DOC_ROOT.parent / "modules"
|
||||
if _modules_root.is_dir():
|
||||
for _doc_dir in sorted(_modules_root.glob("*/doc")):
|
||||
for _img in _doc_dir.rglob("*"):
|
||||
if not (_img.is_file() and _img.suffix.lower() in _IMAGE_EXTS):
|
||||
continue
|
||||
if _img.name in _IMAGE_INDEX:
|
||||
continue
|
||||
_IMAGE_INDEX[_img.name] = f"api_pics/{_img.name}"
|
||||
if _stage_pics:
|
||||
_link = _api_pics / _img.name
|
||||
if not _link.exists():
|
||||
try:
|
||||
_os.symlink(_img, _link)
|
||||
except (OSError, NotImplementedError):
|
||||
try:
|
||||
_shutil.copy2(_img, _link)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if _API_XML_DIR.is_dir():
|
||||
_patch_namespace_xml_for_breathe(_API_XML_DIR, _PATCHED_XML_DIR)
|
||||
|
||||
from conf_helpers.state import OPENCV_ROOT, CONTRIB_ROOT
|
||||
_is_contrib = lambda m: (CONTRIB_ROOT / m).is_dir() and not (
|
||||
OPENCV_ROOT / "modules" / m).is_dir()
|
||||
_main_api = [m for m in API_MODULES if not _is_contrib(m)]
|
||||
_extra_api = [m for m in API_MODULES if _is_contrib(m)]
|
||||
_main_orphans, _extra_orphans = _discover_orphan_groups(_API_XML_DIR)
|
||||
_generate_api_stubs(_main_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "main_modules",
|
||||
root_anchor="api_root", root_title="Main modules",
|
||||
extra_groups=_main_orphans)
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "main_modules")
|
||||
if _extra_api or _extra_orphans:
|
||||
_generate_api_stubs(_extra_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "extra_modules",
|
||||
root_anchor="extra_api_root", root_title="Extra modules",
|
||||
extra_groups=_extra_orphans)
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "extra_modules")
|
||||
|
||||
|
||||
def _write_root_index() -> None:
|
||||
"""Generate the Sphinx landing page at ``index.html``.
|
||||
|
||||
The legacy tutorials root remains focused on C++ tutorials. Cross-family
|
||||
entry points live here so the site root no longer redirects users straight
|
||||
to ``tutorials/tutorials.html``.
|
||||
|
||||
Each entry renders as a section heading (the category) with the page link
|
||||
on the line beneath it. FAQ and Bibliography are direct links whose heading
|
||||
*is* the link. A hidden toctree mirrors the same order to drive the sidebar.
|
||||
"""
|
||||
if SPHINX_INPUT_ROOT == DOC_ROOT:
|
||||
return
|
||||
|
||||
entries: list[tuple[str, str | None, str]] = []
|
||||
|
||||
def add(heading: str, link_text: str | None, docname: str,
|
||||
condition: bool = True) -> None:
|
||||
if condition:
|
||||
entries.append((heading, link_text, docname))
|
||||
|
||||
add("Introduction", "Introduction", "intro", "intro" in _ANCHOR_TO_DOC)
|
||||
add("OpenCV Tutorials", "OpenCV tutorials", "tutorials/tutorials")
|
||||
add("Python Tutorials", "OpenCV-Python tutorials",
|
||||
"py_tutorials/py_tutorials", bool(PY_DOC_MODULES))
|
||||
add("Javascript Tutorials", "OpenCV.js tutorials",
|
||||
"js_tutorials/js_tutorials", bool(JS_DOC_MODULES))
|
||||
add("Contrib Tutorials", "Tutorials for contrib module",
|
||||
f"tutorials_contrib/{_contrib_root_md.stem}",
|
||||
bool(CONTRIB_MODULES) and _contrib_root_md.is_file())
|
||||
add("Main modules", "Documentation for main modules",
|
||||
"main_modules/api_root", "api_root" in _ANCHOR_TO_DOC)
|
||||
add("Extra modules", "Documentation for extra modules",
|
||||
"extra_modules/api_root", "extra_api_root" in _ANCHOR_TO_DOC)
|
||||
add("Frequently Asked Questions", None, "faq", "faq" in _ANCHOR_TO_DOC)
|
||||
add("Bibliography", None, "citelist", "citelist" in _ANCHOR_TO_DOC)
|
||||
|
||||
toctree = "\n".join(
|
||||
f"{heading} <{docname}>" for heading, _link, docname in entries)
|
||||
|
||||
# Body: raw HTML so links resolve correctly relative to index.html.
|
||||
html_lines = ['<div class="ocv-landing">']
|
||||
for heading, link_text, docname in entries:
|
||||
if link_text is None:
|
||||
html_lines.append(
|
||||
f'<h2><a href="{docname}.html">{heading}</a></h2>')
|
||||
else:
|
||||
html_lines.append(f'<h2>{heading}</h2>')
|
||||
html_lines.append(f'<p><a href="{docname}.html">{link_text}</a></p>')
|
||||
html_lines.append("</div>")
|
||||
body = "\n".join(html_lines)
|
||||
|
||||
text = (
|
||||
"OpenCV modules\n"
|
||||
"==============\n\n"
|
||||
"```{toctree}\n"
|
||||
":hidden:\n"
|
||||
":maxdepth: 1\n"
|
||||
":titlesonly:\n\n"
|
||||
f"{toctree}\n"
|
||||
"```\n\n"
|
||||
f"{body}\n"
|
||||
)
|
||||
try:
|
||||
(SPHINX_INPUT_ROOT / "index.markdown").write_text(text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _write_related_pages_index() -> None:
|
||||
"""Generate `related_pages.markdown` — the local analog of Doxygen's
|
||||
pages.html (the header "Related Pages" target).
|
||||
|
||||
Lists every standalone documentation page (\\page) that has a *local*
|
||||
Sphinx docname, so nothing points off-site. Titles and the canonical set
|
||||
come from the Doxygen tag page index (`_DOC_PAGE_TITLES`); a page is
|
||||
emitted only when its name resolves through `_ANCHOR_TO_DOC`, so the list
|
||||
contains exactly what this build actually rendered and grows automatically
|
||||
as more modules are enabled. Marked `orphan` — reached via the header link,
|
||||
not the sidebar toctree (intro/faq/citelist already live in the index toc).
|
||||
"""
|
||||
if SPHINX_INPUT_ROOT == DOC_ROOT:
|
||||
return
|
||||
rows: list[tuple[str, str]] = [] # (title, docname)
|
||||
seen: set[str] = set()
|
||||
|
||||
def add(anchor: str) -> None:
|
||||
doc = _ANCHOR_TO_DOC.get(anchor)
|
||||
if doc and anchor not in seen:
|
||||
title = (_DOC_PAGE_TITLES.get(anchor)
|
||||
or _ANCHOR_TO_TITLE.get(anchor) or anchor)
|
||||
rows.append((title, doc))
|
||||
seen.add(anchor)
|
||||
|
||||
# Core standalone pages first, in a stable, friendly order.
|
||||
for _a in ("intro", "faq", "citelist"):
|
||||
add(_a)
|
||||
# Then every other \page that resolves locally, alphabetical by title.
|
||||
for _name in sorted(_DOC_PAGE_TITLES,
|
||||
key=lambda n: (_DOC_PAGE_TITLES.get(n) or n).lower()):
|
||||
add(_name)
|
||||
|
||||
items = "\n".join(f'<li><a href="{_d}.html">{_t}</a></li>' for _t, _d in rows)
|
||||
text = (
|
||||
"---\norphan: true\n---\n"
|
||||
"# Related Pages\n\n"
|
||||
"All standalone documentation pages available in this build.\n\n"
|
||||
f'<ul class="ocv-related-pages">\n{items}\n</ul>\n'
|
||||
)
|
||||
try:
|
||||
(SPHINX_INPUT_ROOT / "related_pages.markdown").write_text(
|
||||
text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _write_examples_index() -> None:
|
||||
"""Generate `examples/examples_root.markdown` — the local analog of
|
||||
Doxygen's examples.html (the header "Examples" target).
|
||||
|
||||
The per-sample example pages are orphan pages reached from class "Examples"
|
||||
blocks; this index links them all in one place. Sourced from
|
||||
`_EXAMPLE_PAGES_NEEDED` (populated during API-stub generation), so it lists
|
||||
exactly the samples this build emitted. Also `orphan` (header-only entry).
|
||||
"""
|
||||
if SPHINX_INPUT_ROOT == DOC_ROOT:
|
||||
return
|
||||
from .examples import _EXAMPLE_PAGES_NEEDED, _example_pagename
|
||||
if not _EXAMPLE_PAGES_NEEDED:
|
||||
return
|
||||
items = "\n".join(
|
||||
f'<li><a href="{_example_pagename(_d)}.html">{_d}</a></li>'
|
||||
for _d in sorted(_EXAMPLE_PAGES_NEEDED))
|
||||
text = (
|
||||
"---\norphan: true\n---\n"
|
||||
"# Examples\n\n"
|
||||
"All example programs referenced in the API documentation.\n\n"
|
||||
f'<ul class="ocv-examples-index">\n{items}\n</ul>\n'
|
||||
)
|
||||
try:
|
||||
(SPHINX_INPUT_ROOT / "examples").mkdir(parents=True, exist_ok=True)
|
||||
(SPHINX_INPUT_ROOT / "examples" / "examples_root.markdown").write_text(
|
||||
text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _esc(s: str) -> str:
|
||||
"""Minimal HTML escape for brief text injected into the index <li> markup."""
|
||||
return (s or "").replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def _write_namespace_list_index() -> None:
|
||||
"""Generate `namespace_list.markdown` — local analog of Doxygen's
|
||||
namespaces.html (the header "Namespaces" target).
|
||||
|
||||
Renders the namespace tree (cv → cv::cuda → …) as a nested list, each node
|
||||
linking to its local namespace page with the brief description alongside.
|
||||
Intermediate namespaces with no page of their own render as plain text.
|
||||
Sourced from `_ALL_NAMESPACES` (populated during API-stub generation).
|
||||
"""
|
||||
if SPHINX_INPUT_ROOT == DOC_ROOT or not _ALL_NAMESPACES:
|
||||
return
|
||||
# Nested tree keyed by path component; each node tracks its full name.
|
||||
tree: dict = {}
|
||||
for _name in _ALL_NAMESPACES:
|
||||
node = tree
|
||||
parts = _name.split("::")
|
||||
for _i, _part in enumerate(parts):
|
||||
node = node.setdefault(
|
||||
_part, {"_full": "::".join(parts[:_i + 1]), "_kids": {}})["_kids"]
|
||||
|
||||
def render(node: dict) -> list[str]:
|
||||
out = ["<ul>"]
|
||||
for _part in sorted(node, key=str.lower):
|
||||
child = node[_part]
|
||||
info = _ALL_NAMESPACES.get(child["_full"])
|
||||
if info:
|
||||
label = f'<a href="{info["docname"]}.html">{_part}</a>'
|
||||
if info.get("brief"):
|
||||
label += f' — {_esc(info["brief"])}'
|
||||
else:
|
||||
label = _part
|
||||
out.append(f"<li>{label}")
|
||||
if child["_kids"]:
|
||||
out += render(child["_kids"])
|
||||
out.append("</li>")
|
||||
out.append("</ul>")
|
||||
return out
|
||||
|
||||
text = (
|
||||
"---\norphan: true\n---\n"
|
||||
"# Namespace List\n\n"
|
||||
"Here is a list of all documented namespaces with brief descriptions.\n\n"
|
||||
f'<div class="ocv-namespace-list">\n{chr(10).join(render(tree))}\n</div>\n'
|
||||
)
|
||||
try:
|
||||
(SPHINX_INPUT_ROOT / "namespace_list.markdown").write_text(
|
||||
text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _write_class_list_index() -> None:
|
||||
"""Generate `class_list.markdown` — local analog of Doxygen's annotated.html
|
||||
(the header "Classes" target).
|
||||
|
||||
Lists every documented class/struct grouped by its enclosing namespace,
|
||||
each linking to its local page with the brief description. Sourced from
|
||||
`_ALL_CLASSES` (populated during API-stub generation).
|
||||
"""
|
||||
if SPHINX_INPUT_ROOT == DOC_ROOT or not _ALL_CLASSES:
|
||||
return
|
||||
by_ns: dict[str, list[tuple[str, dict]]] = {}
|
||||
for _info in _ALL_CLASSES.values():
|
||||
qualified = _info.get("qualified", "")
|
||||
if not qualified:
|
||||
continue
|
||||
ns, _, leaf = qualified.rpartition("::")
|
||||
by_ns.setdefault(ns, []).append((leaf, _info))
|
||||
|
||||
body = ['<ul class="ocv-class-list">']
|
||||
for ns in sorted(by_ns, key=lambda n: (n == "", n.lower())):
|
||||
heading = ns if ns else "(global namespace)"
|
||||
ns_info = _ALL_NAMESPACES.get(ns)
|
||||
if ns_info:
|
||||
heading = f'<a href="{ns_info["docname"]}.html">{ns}</a>'
|
||||
body.append(f"<li><b>{heading}</b>")
|
||||
body.append("<ul>")
|
||||
for leaf, info in sorted(by_ns[ns], key=lambda t: t[0].lower()):
|
||||
entry = f'<a href="{info["docname"]}.html">{leaf}</a>'
|
||||
if info.get("brief"):
|
||||
entry += f' — {_esc(info["brief"])}'
|
||||
body.append(f"<li>{entry}</li>")
|
||||
body.append("</ul></li>")
|
||||
body.append("</ul>")
|
||||
|
||||
text = (
|
||||
"---\norphan: true\n---\n"
|
||||
"# Class List\n\n"
|
||||
"Here are the classes, structs and unions with brief descriptions.\n\n"
|
||||
f'<div class="ocv-class-list-wrap">\n{chr(10).join(body)}\n</div>\n'
|
||||
)
|
||||
try:
|
||||
(SPHINX_INPUT_ROOT / "class_list.markdown").write_text(
|
||||
text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_write_root_index()
|
||||
_write_related_pages_index()
|
||||
if API_MODULES:
|
||||
_write_examples_index()
|
||||
_write_namespace_list_index()
|
||||
_write_class_list_index()
|
||||
|
||||
for _toc in (DOC_ROOT / "tutorials").glob("*/table_of_content_*.markdown"):
|
||||
if _toc.parent.name not in DOC_MODULES:
|
||||
_scan_external(_toc)
|
||||
# Same for js_tutorials (files are named js_table_of_contents_*.markdown there).
|
||||
for _toc in (DOC_ROOT / "js_tutorials").glob("*/js_table_of_contents_*.markdown"):
|
||||
if _toc.parent.name not in JS_DOC_MODULES:
|
||||
_scan_external(_toc)
|
||||
# py_tutorials uses the `py_table_of_contents_*.markdown` naming variant.
|
||||
for _toc in (DOC_ROOT / "py_tutorials").glob("*/py_table_of_contents_*.markdown"):
|
||||
if _toc.parent.name not in PY_DOC_MODULES:
|
||||
_scan_external(_toc)
|
||||
|
||||
_REFERENCED_ANCHORS.update({
|
||||
"intro", "faq", "citelist",
|
||||
"tutorial_js_root", "tutorial_py_root", "tutorial_contrib_root",
|
||||
"api_root", "extra_api_root",
|
||||
})
|
||||
|
||||
# Snippet basename index (mirrors Doxygen EXAMPLE_RECURSIVE lookup).
|
||||
_SNIPPET_EXTENSIONS = {
|
||||
".cpp", ".hpp", ".h", ".c", ".cc", ".cxx",
|
||||
".py", ".java", ".kt", ".scala", ".clj", ".groovy",
|
||||
".sh", ".bash", ".bat", ".ps1",
|
||||
".cmake", ".gradle",
|
||||
".xml", ".yaml", ".yml", ".json", ".html", ".css",
|
||||
".js", ".ts", ".rb",
|
||||
}
|
||||
_snippet_scan_roots = [OPENCV_ROOT / "samples", OPENCV_ROOT / "apps"] + [
|
||||
CONTRIB_ROOT / _m / "samples" for _m in CONTRIB_MODULES]
|
||||
for _root in _snippet_scan_roots:
|
||||
if _root.is_dir():
|
||||
for _f in _root.rglob("*"):
|
||||
if _f.is_file() and _f.suffix.lower() in _SNIPPET_EXTENSIONS:
|
||||
_SNIPPET_INDEX.setdefault(_f.name, _f)
|
||||
@@ -0,0 +1,435 @@
|
||||
# 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.
|
||||
|
||||
"""Per-class "Examples" cross-reference system."""
|
||||
from __future__ import annotations
|
||||
import re, pathlib
|
||||
from .state import *
|
||||
|
||||
_EXAMPLE_SOURCE_EXTENSIONS = {
|
||||
# Program file types only; headers excluded.
|
||||
".cpp", ".cc", ".cxx", ".c",
|
||||
".py", ".java", ".js", ".ts",
|
||||
}
|
||||
_EXAMPLE_LANGUAGE = {
|
||||
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".c": "c",
|
||||
".py": "python", ".java": "java",
|
||||
".js": "javascript", ".ts": "typescript",
|
||||
}
|
||||
_EXAMPLE_INCLUDE_SUBTREES = (
|
||||
"samples/cpp/tutorial_code/",
|
||||
"samples/python/",
|
||||
"samples/java/",
|
||||
"samples/dnn/",
|
||||
"samples/gpu/",
|
||||
)
|
||||
|
||||
|
||||
def _is_canonical_example(rel_path: str) -> bool:
|
||||
"""True iff this repo-relative path is a canonical example."""
|
||||
if any(rel_path.startswith(p) for p in _EXAMPLE_INCLUDE_SUBTREES):
|
||||
return True
|
||||
if rel_path.startswith("samples/cpp/"):
|
||||
rest = rel_path[len("samples/cpp/"):]
|
||||
return "/" not in rest # direct child only, not nested
|
||||
if re.match(r"opencv_contrib/modules/[^/]+/samples/", rel_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _example_pagename(display_path: str) -> str:
|
||||
"""`samples/cpp/pca.cpp` → `samples_cpp_pca_cpp` (Sphinx-safe basename)."""
|
||||
return re.sub(r"[^A-Za-z0-9]+", "_", display_path).strip("_").lower()
|
||||
|
||||
|
||||
_EXAMPLE_FILES: list[tuple[str, pathlib.Path, frozenset[str]]] = []
|
||||
_EXAMPLE_TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||||
|
||||
# Not scoped to CONTRIB_MODULES: examples surface for all modules.
|
||||
_example_scan_roots: list[tuple[pathlib.Path, str]] = [
|
||||
(OPENCV_ROOT / "samples", "samples"),
|
||||
]
|
||||
if CONTRIB_ROOT.is_dir():
|
||||
for _module_dir in sorted(CONTRIB_ROOT.iterdir()):
|
||||
if not _module_dir.is_dir():
|
||||
continue
|
||||
_contrib_samples = _module_dir / "samples"
|
||||
if _contrib_samples.is_dir():
|
||||
_example_scan_roots.append((
|
||||
_contrib_samples,
|
||||
f"opencv_contrib/modules/{_module_dir.name}/samples",
|
||||
))
|
||||
|
||||
for _root, _display_prefix in _example_scan_roots:
|
||||
if not _root.is_dir():
|
||||
continue
|
||||
for _f in _root.rglob("*"):
|
||||
if not _f.is_file() or _f.suffix.lower() not in _EXAMPLE_SOURCE_EXTENSIONS:
|
||||
continue
|
||||
_rel = _f.relative_to(_root).as_posix()
|
||||
_display = f"{_display_prefix}/{_rel}"
|
||||
if not _is_canonical_example(_display):
|
||||
continue
|
||||
try:
|
||||
_text = _f.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
_EXAMPLE_FILES.append((
|
||||
_display,
|
||||
_f,
|
||||
frozenset(_EXAMPLE_TOKEN_RE.findall(_text)),
|
||||
))
|
||||
_EXAMPLE_FILES.sort(key=lambda t: t[0])
|
||||
|
||||
# Display path → source path; only referenced samples (avoids orphan pages).
|
||||
_EXAMPLE_PAGES_NEEDED: dict[str, pathlib.Path] = {}
|
||||
|
||||
|
||||
_TUTORIAL_LINK_RE = re.compile(
|
||||
r"\[([^\]]+)\]\(([^)]*?samples/[^)]+?\.(?:cpp|cc|cxx|c|py|java|js|ts))\)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# Trailing connector phrase, stripped from end of description.
|
||||
_TUTORIAL_LINK_TRAILER_RE = re.compile(
|
||||
r"\s*(?:"
|
||||
r"can be found at|can be found in|"
|
||||
r"are available (?:at|in)|is available (?:at|in)|"
|
||||
r"located at|located in|found at|found in|"
|
||||
r"see also|see|here|"
|
||||
r"is at|is in|at|in"
|
||||
r")\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_TUTORIAL_LEADING_DIRECTIVE_RE = re.compile(
|
||||
r"^@(?:note|see|sa|warning|attention|remark|brief|todo|deprecated)\s+",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ANOTHER_LEAD_RE = re.compile(r"^Another\s+(\w)", re.IGNORECASE)
|
||||
|
||||
|
||||
def _extract_link_lead_in(text: str, link_start: int) -> str:
|
||||
"""Pull the cleaned-up prose right before a tutorial hyperlink."""
|
||||
LOOKBACK = 400
|
||||
before = text[max(0, link_start - LOOKBACK):link_start]
|
||||
cut = max(
|
||||
before.rfind("\n\n"),
|
||||
before.rfind(". "),
|
||||
before.rfind("! "),
|
||||
before.rfind("? "),
|
||||
before.rfind(":\n"),
|
||||
)
|
||||
if cut >= 0:
|
||||
before = before[cut + 2:] # skip past the 2-char boundary
|
||||
before = _TUTORIAL_LEADING_DIRECTIVE_RE.sub("", before.lstrip())
|
||||
before = re.sub(r"\s+", " ", before).strip()
|
||||
before = _TUTORIAL_LINK_TRAILER_RE.sub("", before).strip()
|
||||
m = _ANOTHER_LEAD_RE.match(before)
|
||||
if m:
|
||||
article = "An " if m.group(1).lower() in "aeiou" else "A "
|
||||
before = article + before[m.start(1):]
|
||||
return before
|
||||
|
||||
|
||||
def _scan_tutorial_sample_refs() -> dict[str, str]:
|
||||
"""Walk every tutorial markdown for sample-file hyperlinks."""
|
||||
refs: dict[str, str] = {}
|
||||
tutorial_roots = [
|
||||
DOC_ROOT / "tutorials",
|
||||
DOC_ROOT / "js_tutorials",
|
||||
DOC_ROOT / "py_tutorials",
|
||||
]
|
||||
for tut_root in tutorial_roots:
|
||||
if not tut_root.is_dir():
|
||||
continue
|
||||
for md in tut_root.rglob("*.markdown"):
|
||||
try:
|
||||
text = md.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
for m in _TUTORIAL_LINK_RE.finditer(text):
|
||||
sample_m = re.search(
|
||||
r"samples/.+\.(?:cpp|cc|cxx|c|py|java|js|ts)",
|
||||
m.group(2), re.IGNORECASE,
|
||||
)
|
||||
if not sample_m:
|
||||
continue
|
||||
sample = sample_m.group(0)
|
||||
brief = _extract_link_lead_in(text, m.start())
|
||||
if not brief:
|
||||
continue
|
||||
if sample not in refs or len(brief) > len(refs[sample]):
|
||||
refs[sample] = brief
|
||||
return refs
|
||||
|
||||
|
||||
_TUTORIAL_SAMPLE_REFS: dict[str, str] = _scan_tutorial_sample_refs()
|
||||
|
||||
_DOXY_EXAMPLE_RE = re.compile(
|
||||
r"[@\\]example\s+(\S+)" # declared path
|
||||
r"[^\n]*\n"
|
||||
r"(?P<desc>"
|
||||
r"(?:"
|
||||
r"(?!\s*\*/)" # not comment closer
|
||||
r"(?!\s*\*?\s*[@\\]\w+)" # not a new directive
|
||||
r"[^\n]*\n"
|
||||
r")*"
|
||||
r")"
|
||||
)
|
||||
|
||||
_DOXY_PERCENT_ESCAPE_RE = re.compile(r"%(\w+)")
|
||||
|
||||
|
||||
def _resolve_example_path(decl_path: str, header_path: pathlib.Path) -> str | None:
|
||||
"""Resolve a `@example <path>` to our repo-relative display path."""
|
||||
# Case 1: repo-relative.
|
||||
abs_main = OPENCV_ROOT / decl_path
|
||||
if abs_main.is_file():
|
||||
return abs_main.relative_to(OPENCV_ROOT).as_posix()
|
||||
|
||||
# Case 2: module-relative — module root is parent of include/.
|
||||
module_root: pathlib.Path | None = None
|
||||
for p in header_path.parents:
|
||||
if p.name == "include":
|
||||
module_root = p.parent
|
||||
break
|
||||
if module_root is None:
|
||||
return None
|
||||
|
||||
abs_mod = module_root / decl_path
|
||||
if not abs_mod.is_file():
|
||||
return None
|
||||
|
||||
# is_relative_to not startswith: opencv prefixes opencv_contrib.
|
||||
if abs_mod.is_relative_to(OPENCV_ROOT):
|
||||
return abs_mod.relative_to(OPENCV_ROOT).as_posix()
|
||||
contrib_parent = CONTRIB_ROOT.parent.parent
|
||||
if abs_mod.is_relative_to(contrib_parent):
|
||||
return abs_mod.relative_to(contrib_parent).as_posix()
|
||||
return f"opencv_contrib/modules/{module_root.name}/{decl_path}"
|
||||
|
||||
|
||||
def _scan_doxygen_example_decls() -> dict[str, tuple[str, str]]:
|
||||
"""Walk every module header for `@example` declarations."""
|
||||
refs: dict[str, tuple[str, str]] = {}
|
||||
roots = [OPENCV_ROOT / "modules"]
|
||||
if CONTRIB_ROOT.is_dir():
|
||||
roots.append(CONTRIB_ROOT)
|
||||
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for ext in ("*.hpp", "*.h"):
|
||||
for header in root.rglob(ext):
|
||||
# Only <module>/include/.
|
||||
if "/include/" not in header.as_posix():
|
||||
continue
|
||||
try:
|
||||
text = header.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
for m in _DOXY_EXAMPLE_RE.finditer(text):
|
||||
declared = m.group(1).strip().rstrip("*/").strip()
|
||||
resolved = _resolve_example_path(declared, header)
|
||||
if not resolved:
|
||||
continue
|
||||
raw = m.group("desc") or ""
|
||||
cleaned = re.sub(r"\n\s*\*\s?", " ", raw)
|
||||
cleaned = cleaned.replace("*/", "").strip()
|
||||
cleaned = re.sub(r"\s+", " ", cleaned)
|
||||
cleaned = _DOXY_PERCENT_ESCAPE_RE.sub(r"\1", cleaned)
|
||||
# Keep longest if declared in multiple headers.
|
||||
existing = refs.get(resolved)
|
||||
if existing is None or len(cleaned) > len(existing[1]):
|
||||
refs[resolved] = (declared, cleaned)
|
||||
return refs
|
||||
|
||||
|
||||
_DOXY_EXAMPLE_DECLS: dict[str, tuple[str, str]] = _scan_doxygen_example_decls()
|
||||
|
||||
|
||||
def _find_examples_for_class(class_simple: str) -> list[tuple[str, str]]:
|
||||
"""Canonical sample files mentioning the class name."""
|
||||
if not class_simple:
|
||||
return []
|
||||
candidates = {class_simple}
|
||||
if class_simple.startswith("_") and len(class_simple) > 1:
|
||||
candidates.add(class_simple[1:]) # _InputArray → InputArray alias
|
||||
out: list[tuple[str, str]] = []
|
||||
for display, source_path, tokens in _EXAMPLE_FILES:
|
||||
# Stage 1: must be declared with @example.
|
||||
decl = _DOXY_EXAMPLE_DECLS.get(display)
|
||||
if decl is None:
|
||||
continue
|
||||
# Stage 2: must mention the class.
|
||||
if any(c in tokens for c in candidates):
|
||||
_EXAMPLE_PAGES_NEEDED[display] = source_path
|
||||
declared, _desc = decl
|
||||
out.append((declared, _example_pagename(display)))
|
||||
return out
|
||||
|
||||
|
||||
def _render_examples_block(examples: list[tuple[str, str]]) -> list[str]:
|
||||
"""HTML lines for the "Examples" footer; empty list if no matches."""
|
||||
if not examples:
|
||||
return []
|
||||
import html as _html_pkg
|
||||
parts = [
|
||||
f'<a class="opencv-example-link" '
|
||||
f'href="../examples/{_html_pkg.escape(page, quote=True)}.html">'
|
||||
f'{_html_pkg.escape(display)}</a>'
|
||||
for display, page in examples
|
||||
]
|
||||
if len(parts) == 1:
|
||||
joined = parts[0]
|
||||
else:
|
||||
joined = ", ".join(parts[:-1]) + ", and " + parts[-1]
|
||||
return [
|
||||
'<dl class="opencv-examples">',
|
||||
'<dt>Examples</dt>',
|
||||
f'<dd>{joined}.</dd>',
|
||||
'</dl>',
|
||||
"",
|
||||
]
|
||||
|
||||
# Boilerplate-paragraph filter for _extract_sample_brief.
|
||||
_SAMPLE_BRIEF_SKIP_RE = re.compile(
|
||||
r"^(?:"
|
||||
r"author|date|file|copyright|special\s+thanks|see\s+also|maintainer|"
|
||||
r"created|modified|version|license|brief"
|
||||
r")\b"
|
||||
r"|^\w+\.(?:cpp|cc|cxx|c|hpp|h|py|java|js|ts)\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _extract_sample_brief(text: str) -> str:
|
||||
"""Best-effort one-line description of a sample file."""
|
||||
# 1) Explicit @brief / \brief — preferred when present.
|
||||
m = re.search(
|
||||
r"[@\\]brief\s+(.*?)(?:"
|
||||
r"\n\s*[*/]?\s*\n" # paragraph break
|
||||
r"|\n\s*[*/]?\s*[@\\]\w+" # next Doxygen tag
|
||||
r"|\*/" # end of comment
|
||||
r")",
|
||||
text, re.DOTALL,
|
||||
)
|
||||
if m:
|
||||
brief = re.sub(r"\n\s*\*?\s*", " ", m.group(1))
|
||||
brief = re.sub(r"\s+", " ", brief).strip()
|
||||
brief = re.split(r"(?<=[.!?])\s", brief, maxsplit=1)[0].strip()
|
||||
if brief:
|
||||
return brief
|
||||
|
||||
# 2) First /* ... */ block, split into paragraphs.
|
||||
cm = re.match(r"\s*/\*+([\s\S]*?)\*+/", text)
|
||||
if not cm:
|
||||
return ""
|
||||
# .strip() not .rstrip(): leading space would bypass skip regex.
|
||||
norm_lines = [
|
||||
re.sub(r"^\s*\*+\s?", "", raw).strip()
|
||||
for raw in cm.group(1).splitlines()
|
||||
]
|
||||
paragraphs: list[list[str]] = [[]]
|
||||
for line in norm_lines:
|
||||
if line.strip():
|
||||
paragraphs[-1].append(line)
|
||||
elif paragraphs[-1]:
|
||||
paragraphs.append([])
|
||||
paragraphs = [p for p in paragraphs if p]
|
||||
|
||||
for para in paragraphs:
|
||||
if _SAMPLE_BRIEF_SKIP_RE.match(para[0]):
|
||||
continue
|
||||
joined = " ".join(line.strip() for line in para if line.strip())
|
||||
first = re.split(r"(?<=[.!?])\s", joined, maxsplit=1)[0].strip()
|
||||
if first:
|
||||
return first
|
||||
return ""
|
||||
|
||||
_BRIEF_REF_RE = re.compile(r'@ref\s+(?P<anchor>[\w:-]+)(?:\s+"(?P<label>[^"]+)")?')
|
||||
_BRIEF_IMG_RE = re.compile(r'!\[(?P<alt>[^\]]*)\]\((?P<src>[^)]+)\)')
|
||||
|
||||
|
||||
def _resolve_brief_markup(brief: str) -> str:
|
||||
"""Resolve `@ref` links and bare-filename images in a brief to Markdown."""
|
||||
def _ref(m: "re.Match") -> str:
|
||||
anchor = _resolve_redirect(m.group("anchor"))
|
||||
label = m.group("label")
|
||||
target = _ANCHOR_TO_DOC.get(anchor)
|
||||
if target:
|
||||
return f'[{label or _ANCHOR_TO_TITLE.get(anchor) or anchor}](/{target})'
|
||||
if anchor in _TAG_FILENAMES:
|
||||
return f'[{label or _TAG_TITLES.get(anchor, anchor)}]({_doxygen_url(anchor)})'
|
||||
return f'[{label or anchor}](#{anchor})'
|
||||
brief = _BRIEF_REF_RE.sub(_ref, brief)
|
||||
|
||||
def _img(m: "re.Match") -> str:
|
||||
alt, src = m.group("alt"), m.group("src")
|
||||
# Leave already-pathed / absolute / URL images for MyST to handle.
|
||||
if "/" in src or "://" in src:
|
||||
return m.group(0)
|
||||
hit = _IMAGE_INDEX.get(src)
|
||||
if not hit:
|
||||
return m.group(0)
|
||||
# Hard break after image so following prose drops to a new line.
|
||||
return f'\\\n'
|
||||
return _BRIEF_IMG_RE.sub(_img, brief).strip()
|
||||
|
||||
|
||||
def _generate_example_pages(examples_dir: pathlib.Path) -> None:
|
||||
"""Write one Sphinx page per sample referenced by an Examples block.
|
||||
|
||||
Uses MyST colon-fence `:::` not backticks: source backticks can
|
||||
close a backtick fence prematurely.
|
||||
"""
|
||||
if not _EXAMPLE_PAGES_NEEDED:
|
||||
return
|
||||
examples_dir.mkdir(parents=True, exist_ok=True)
|
||||
for display, source in _EXAMPLE_PAGES_NEEDED.items():
|
||||
try:
|
||||
body = source.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
language = _EXAMPLE_LANGUAGE.get(source.suffix.lower(), "text")
|
||||
# Brief cascade: @example desc, tutorial prose, top comment.
|
||||
decl = _DOXY_EXAMPLE_DECLS.get(display)
|
||||
declared_path = decl[0] if decl else display
|
||||
decl_desc = decl[1] if decl else ""
|
||||
brief = (
|
||||
decl_desc
|
||||
or _TUTORIAL_SAMPLE_REFS.get(display)
|
||||
or _extract_sample_brief(body)
|
||||
)
|
||||
lines = [
|
||||
"---",
|
||||
"orphan: true",
|
||||
"---",
|
||||
f"# {declared_path}",
|
||||
"",
|
||||
]
|
||||
if brief:
|
||||
# MyST paragraph (not raw <p>) so embedded @ref / image renders.
|
||||
lines.append("{.opencv-example-brief}")
|
||||
lines.append(_resolve_brief_markup(brief))
|
||||
lines.append("")
|
||||
lines.extend([
|
||||
f":::{{code-block}} {language}",
|
||||
":linenos:",
|
||||
"",
|
||||
])
|
||||
lines.extend(body.splitlines())
|
||||
lines.append(":::")
|
||||
lines.append("")
|
||||
(examples_dir / f"{_example_pagename(display)}.md").write_text(
|
||||
"\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_find_examples_for_class",
|
||||
"_render_examples_block",
|
||||
"_generate_example_pages",
|
||||
]
|
||||
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
|
||||
"""Runtime patches for Sphinx C++ domain and breathe; applied at import."""
|
||||
from __future__ import annotations
|
||||
|
||||
def _patch_cpp_xref_resolver():
|
||||
"""Work around Sphinx 8.1.x parentSymbol assert in _resolve_xref_inner."""
|
||||
try:
|
||||
from sphinx.domains.cpp import CPPDomain
|
||||
except ImportError:
|
||||
return
|
||||
original = CPPDomain._resolve_xref_inner
|
||||
|
||||
def guarded(self, env, fromdocname, builder, typ, target, node, contnode):
|
||||
try:
|
||||
return original(self, env, fromdocname, builder, typ, target,
|
||||
node, contnode)
|
||||
except AssertionError:
|
||||
return None, None
|
||||
CPPDomain._resolve_xref_inner = guarded
|
||||
|
||||
# Drop breathe unresolvable-xref log noise; text still renders.
|
||||
import logging
|
||||
_UNRESOLVED_XREF_PATTERNS = (
|
||||
"Unable to resolve function",
|
||||
"Unable to resolve class",
|
||||
"Cannot find function",
|
||||
"Cannot find class",
|
||||
"Cannot find variable",
|
||||
"Cannot find typedef",
|
||||
"Cannot find enum",
|
||||
"Cannot find enumerator",
|
||||
"Cannot find define",
|
||||
"Duplicate C++ declaration",
|
||||
)
|
||||
|
||||
class _UnresolvedXrefFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
msg = record.getMessage()
|
||||
return not any(p in msg for p in _UNRESOLVED_XREF_PATTERNS)
|
||||
|
||||
_filt = _UnresolvedXrefFilter()
|
||||
for _logger_name in ("sphinx", "docutils"):
|
||||
logging.getLogger(_logger_name).addFilter(_filt)
|
||||
|
||||
|
||||
_patch_cpp_xref_resolver()
|
||||
|
||||
|
||||
def _silence_breathe_anon_enum_warning():
|
||||
"""Mute Sphinx parser warning on Doxygen's anonymous nested enums."""
|
||||
import logging
|
||||
class _AnonEnumFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
msg = record.getMessage()
|
||||
return not (
|
||||
"Invalid C++ declaration" in msg
|
||||
and "Expected identifier in nested name" in msg
|
||||
)
|
||||
for _name in ("sphinx", "docutils"):
|
||||
logging.getLogger(_name).addFilter(_AnonEnumFilter())
|
||||
|
||||
|
||||
_silence_breathe_anon_enum_warning()
|
||||
|
||||
|
||||
def _patch_breathe_operator_signatures():
|
||||
"""Fix breathe {doxygenfunction} mis-splitting operator overloads."""
|
||||
try:
|
||||
import breathe.directives.function as _bf
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
def _split_operator(s: str):
|
||||
rp = s.rfind(")")
|
||||
if rp == -1:
|
||||
return None
|
||||
depth, j = 0, rp
|
||||
while j >= 0:
|
||||
if s[j] == ")":
|
||||
depth += 1
|
||||
elif s[j] == "(":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j -= 1
|
||||
if j < 0:
|
||||
return None
|
||||
func_part, args_part = s[:j].strip(), s[j:]
|
||||
k = func_part.find("::operator")
|
||||
if k != -1:
|
||||
return func_part[:k], func_part[k + 2:], args_part
|
||||
if "::" in func_part:
|
||||
ns, fn = func_part.rsplit("::", 1)
|
||||
return ns, fn, args_part
|
||||
return "", func_part, args_part
|
||||
|
||||
class _Shim:
|
||||
__slots__ = ("_g",)
|
||||
|
||||
def __init__(self, g1, g2, g3):
|
||||
self._g = (None, g1, g2, g3)
|
||||
|
||||
def group(self, i=0):
|
||||
return self._g[i]
|
||||
|
||||
class _OperatorAwareRe:
|
||||
def __init__(self, real):
|
||||
object.__setattr__(self, "_real", real)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._real, name)
|
||||
|
||||
def match(self, pattern, string, *args, **kwargs):
|
||||
m = self._real.match(pattern, string, *args, **kwargs)
|
||||
if (m is not None and getattr(m.re, "groups", 0) >= 3
|
||||
and "::operator" in string):
|
||||
res = _split_operator(string)
|
||||
if res is not None:
|
||||
ns, fn, ar = res
|
||||
return _Shim(ns or None, fn, ar)
|
||||
return m
|
||||
|
||||
if not isinstance(_bf.re, _OperatorAwareRe):
|
||||
_bf.re = _OperatorAwareRe(_bf.re)
|
||||
|
||||
|
||||
_patch_breathe_operator_signatures()
|
||||
|
||||
|
||||
def _patch_breathe_docsect():
|
||||
"""Render title-less docSectN nodes breathe 4.36 drops."""
|
||||
try:
|
||||
from breathe.renderer import sphinxrenderer as _bsr
|
||||
except ImportError:
|
||||
return
|
||||
_methods = _bsr.SphinxRenderer.methods
|
||||
if getattr(_methods.get("docsect1"), "_opencv_docsect_patch", False):
|
||||
return
|
||||
_orig_visit = _methods["docsect1"]
|
||||
|
||||
def _visit_docsectN(self, node):
|
||||
if not getattr(node, "title", None):
|
||||
return self.render_iterable(node.content_)
|
||||
return _orig_visit(self, node)
|
||||
|
||||
_visit_docsectN._opencv_docsect_patch = True
|
||||
for _kind in ("docsect1", "docsect2", "docsect3"):
|
||||
_methods[_kind] = _visit_docsectN
|
||||
|
||||
|
||||
_patch_breathe_docsect()
|
||||
|
||||
|
||||
def _silence_orphan_toctree_warning():
|
||||
"""Mute toctree-orphan warning for intentionally unlinked external pages."""
|
||||
import logging
|
||||
|
||||
class _OrphanFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return "included in any toctree" not in record.getMessage()
|
||||
|
||||
for _name in ("sphinx", "docutils"):
|
||||
logging.getLogger(_name).addFilter(_OrphanFilter())
|
||||
|
||||
|
||||
_silence_orphan_toctree_warning()
|
||||
@@ -0,0 +1,518 @@
|
||||
# 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.
|
||||
|
||||
"""build-finished hook: inline coll-diagram SVGs, strip Breathe clutter, re-theme Doxygen."""
|
||||
from __future__ import annotations
|
||||
import pathlib, re
|
||||
|
||||
from .state import (_doxy_page_to_local, _DOXY_ANCHOR_TO_MEMBER, DOXYGEN_BASE_URL,
|
||||
_LOCAL_CLASS_URL, _LOCAL_TYPEDEF_URL, _FILE_URL, _API_XML_DIR, DOC_ROOT)
|
||||
|
||||
|
||||
def _doxy_parent_page(page: str, api_dir: pathlib.Path) -> str:
|
||||
"""Nested types (e.g. `structcv_1_1SparseMat_1_1Hdr`) get no standalone
|
||||
Sphinx page — they're documented inline on the enclosing class. Walk up the
|
||||
`_1_1`-separated scope to the nearest ancestor page that DOES exist locally.
|
||||
Returns "" if no ancestor page exists."""
|
||||
stem = page[:-5] if page.endswith(".html") else page
|
||||
rest = None
|
||||
for pref in ("class", "struct", "union"):
|
||||
if stem.startswith(pref):
|
||||
rest = stem[len(pref):]
|
||||
break
|
||||
if rest is None:
|
||||
return ""
|
||||
while "_1_1" in rest:
|
||||
rest = rest.rsplit("_1_1", 1)[0]
|
||||
for pref in ("class", "struct", "union"):
|
||||
cand = f"{pref}{rest}.html"
|
||||
if (api_dir / cand).is_file():
|
||||
return cand
|
||||
return ""
|
||||
|
||||
|
||||
def _inline_collaboration_svgs(api_dir: pathlib.Path,
|
||||
image_dir: pathlib.Path) -> None:
|
||||
"""Inline coll-diagram SVGs so their links work; idempotent."""
|
||||
import re
|
||||
if not api_dir.is_dir():
|
||||
return
|
||||
img_re = re.compile(
|
||||
r'<img alt="(?P<alt>[^"]*)" '
|
||||
r'class="(?P<cls>opencv-coll-graph[^"]*)" '
|
||||
r'src="\.\./_images/(?P<file>[^"]+\.svg)"\s*/?>')
|
||||
href_re = re.compile(r'xlink:href="(?P<path>[^"]+)"')
|
||||
|
||||
def _rewrite_href(m: "re.Match") -> str:
|
||||
path = m.group("path")
|
||||
if "://" in path:
|
||||
return m.group(0)
|
||||
base = path.rsplit("/", 1)[-1]
|
||||
page, _, frag = base.partition("#")
|
||||
local = _doxy_page_to_local(page)
|
||||
if not (api_dir / local).is_file():
|
||||
# Nested type with no own page -> enclosing class page (inline docs).
|
||||
parent = _doxy_parent_page(page, api_dir)
|
||||
if parent:
|
||||
local = parent
|
||||
member = _DOXY_ANCHOR_TO_MEMBER.get(frag) if frag else None
|
||||
if member:
|
||||
return f'xlink:href="{local}#{member}"'
|
||||
return f'xlink:href="{local}"'
|
||||
|
||||
for html in api_dir.glob("*.html"):
|
||||
text = html.read_text(encoding="utf-8")
|
||||
if "opencv-coll-graph" not in text:
|
||||
continue
|
||||
|
||||
def _inline(m: "re.Match") -> str:
|
||||
svg_path = image_dir / m.group("file")
|
||||
if not svg_path.is_file():
|
||||
return m.group(0)
|
||||
svg = svg_path.read_text(encoding="utf-8")
|
||||
start = svg.find("<svg")
|
||||
if start < 0:
|
||||
return m.group(0)
|
||||
svg = href_re.sub(_rewrite_href, svg[start:])
|
||||
# carry theme classes + alt for dark mode / a11y
|
||||
svg = svg.replace(
|
||||
"<svg ",
|
||||
f'<svg class="{m.group("cls")}" role="img" '
|
||||
f'aria-label="{m.group("alt")}" ', 1)
|
||||
# Wrap in a scroll box so large graphs (e.g. file include graphs)
|
||||
# stay fully reachable instead of being clipped — scrolls when wider
|
||||
# than the content area, fits otherwise.
|
||||
return f'<div class="opencv-graph-scroll">{svg}</div>'
|
||||
|
||||
new = img_re.sub(_inline, text)
|
||||
if new != text:
|
||||
html.write_text(new, encoding="utf-8")
|
||||
|
||||
|
||||
def _strip_breathe_class_clutter(api_dir: pathlib.Path) -> None:
|
||||
"""Drop Breathe's duplicate class signature header; idempotent."""
|
||||
import re
|
||||
if not api_dir.is_dir():
|
||||
return
|
||||
section_re = re.compile(
|
||||
r'(<section id="detailed-description"[^>]*>)'
|
||||
r'(?P<body>[\s\S]*?)'
|
||||
r'(</section>)'
|
||||
)
|
||||
dl_re = re.compile(
|
||||
r'<dl[^>]*\bclass="[^"]*\bclass\b[^"]*"[^>]*>\s*'
|
||||
r'<dt[^>]*>[\s\S]*?</dt>\s*'
|
||||
r'<dd>(?P<dd>[\s\S]*?)</dd>\s*'
|
||||
r'</dl>'
|
||||
)
|
||||
subclassed_re = re.compile(r'<p>Subclassed by[\s\S]*?</p>\s*')
|
||||
|
||||
for h in api_dir.glob("classcv*.html"):
|
||||
text = h.read_text(encoding="utf-8")
|
||||
if "detailed-description" not in text:
|
||||
continue
|
||||
|
||||
def _strip_section(sm):
|
||||
head, body, tail = sm.group(1), sm.group("body"), sm.group(3)
|
||||
|
||||
def _strip_dl(dm):
|
||||
dd_body = dm.group("dd").strip()
|
||||
dd_body = subclassed_re.sub("", dd_body).strip()
|
||||
return dd_body
|
||||
|
||||
new_body = dl_re.sub(_strip_dl, body, count=1)
|
||||
return head + new_body + tail
|
||||
|
||||
new = section_re.sub(_strip_section, text, count=1)
|
||||
if new != text:
|
||||
h.write_text(new, encoding="utf-8")
|
||||
|
||||
def _fix_gapi_images(out_dir: pathlib.Path) -> None:
|
||||
"""Copy gapi doc images to _images/ and fix src paths in gapi.html."""
|
||||
gapi_html = out_dir / "extra_modules" / "gapi.html"
|
||||
if not gapi_html.is_file():
|
||||
return
|
||||
images_dir = out_dir / "_images"
|
||||
images_dir.mkdir(exist_ok=True)
|
||||
|
||||
text = gapi_html.read_text(encoding="utf-8")
|
||||
|
||||
def _fix_src(m):
|
||||
raw_path = m.group(1)
|
||||
if "contrib_modules" not in raw_path:
|
||||
return m.group(0)
|
||||
# raw_path is relative to extra_modules/ in the browser,
|
||||
# but the file lives at out_dir / raw_path (no extra_modules prefix)
|
||||
src_file = out_dir / raw_path
|
||||
if not src_file.is_file():
|
||||
return m.group(0)
|
||||
dest = images_dir / src_file.name
|
||||
if not dest.is_file():
|
||||
import shutil as _shutil
|
||||
_shutil.copy2(src_file, dest)
|
||||
return f'src="../_images/{src_file.name}"'
|
||||
|
||||
new_text = re.sub(r'src="([^"]+)"', _fix_src, text)
|
||||
if new_text != text:
|
||||
gapi_html.write_text(new_text, encoding="utf-8")
|
||||
|
||||
def _copy_js_tryit_files(out_dir: pathlib.Path) -> None:
|
||||
"""Copy js_*.html Try-it pages + assets so iframe src="../../js_*.html" resolves."""
|
||||
import shutil, os
|
||||
js_assets = DOC_ROOT / "js_tutorials" / "js_assets"
|
||||
dest = out_dir / "js_tutorials"
|
||||
if not js_assets.is_dir() or not dest.is_dir():
|
||||
return
|
||||
for src in js_assets.iterdir():
|
||||
if src.is_file():
|
||||
dst = dest / src.name
|
||||
if not dst.exists():
|
||||
shutil.copy2(src, dst)
|
||||
# opencv.js from CMake (OPENCV_JS_PATH); bundle it alongside the Try-it pages.
|
||||
opencv_js = os.environ.get("OPENCV_JS_PATH", "")
|
||||
if opencv_js and pathlib.Path(opencv_js).is_file():
|
||||
dst = dest / "opencv.js"
|
||||
if not dst.exists():
|
||||
shutil.copy2(opencv_js, dst)
|
||||
# Extra assets referenced by Try-it pages but not in js_assets/.
|
||||
_opencv_root = DOC_ROOT.parent
|
||||
for _name, _src in {
|
||||
"box.mp4": _opencv_root / "samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/box.mp4",
|
||||
"space_shuttle.jpg": DOC_ROOT / "tutorials/dnn/images/space_shuttle.jpg",
|
||||
"roi.jpg": DOC_ROOT / "py_tutorials/py_core/py_basic_ops/images/roi.jpg",
|
||||
}.items():
|
||||
if _src.is_file() and not (dest / _name).exists():
|
||||
shutil.copy2(_src, dest / _name)
|
||||
|
||||
|
||||
def _generate_search_map(out_dir: pathlib.Path) -> None:
|
||||
"""Write _static/search_map.js: stem→Sphinx-path for every built HTML page."""
|
||||
import json
|
||||
skip = {"_static", "_sources", "_images", "_sphinx_design_static"}
|
||||
mapping = {}
|
||||
for f in out_dir.rglob("*.html"):
|
||||
rel = f.relative_to(out_dir)
|
||||
if rel.parts[0] in skip:
|
||||
continue
|
||||
mapping[f.stem] = rel.as_posix()
|
||||
lines = ["var sphinxPageMap = {"]
|
||||
for k, v in sorted(mapping.items()):
|
||||
lines.append(f" {json.dumps(k)}: {json.dumps(v)},")
|
||||
lines.append("};")
|
||||
(out_dir / "_static" / "search_map.js").write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
_SYM = r"(?:group__|classcv|structcv|unioncv|namespacecv)\w*\.html"
|
||||
|
||||
def _localize_doxygen_links(out_dir: pathlib.Path) -> None:
|
||||
"""Point symbol-page links at the local Sphinx page when we built it:
|
||||
docs.opencv.org URLs, and bare relative names that 404 off the API dir."""
|
||||
import os
|
||||
skip = {"_static", "_sources", "_images", "_sphinx_design_static"}
|
||||
page_paths: dict[str, str] = {}
|
||||
for f in out_dir.rglob("*.html"):
|
||||
rel = f.relative_to(out_dir)
|
||||
if rel.parts and rel.parts[0] in skip:
|
||||
continue
|
||||
page_paths.setdefault(f.name, rel.as_posix())
|
||||
ext_re = re.compile(
|
||||
r'(?P<a><a class="reference external"\s+)?'
|
||||
r'href="' + re.escape(DOXYGEN_BASE_URL) + r'(?:[\w-]+/)*?'
|
||||
r'(?P<page>' + _SYM + r')(?:#(?P<frag>\w+))?"')
|
||||
bare_re = re.compile(r'href="(?P<page>' + _SYM + r')(?:#(?P<frag>[\w:.-]+))?"')
|
||||
# `#include` links -> local file-reference page (same Doxygen file stem).
|
||||
inc_re = re.compile(
|
||||
r'<a class="reference external opencv-include-link" '
|
||||
r'href="[^"]*doc/doxygen/html/[^"]*?(?P<file>[^/"]+\.html)">(?P<path>[^<]+)</a>')
|
||||
strip_re = re.compile(
|
||||
r'<a\b[^>]*\bhref="(?:' + re.escape(DOXYGEN_BASE_URL) + r'|[^"]*doc/doxygen/html/)'
|
||||
r'(?![^"]*javadoc)[^"]*"[^>]*>(?P<txt>[^<]*)</a>')
|
||||
|
||||
for html in out_dir.rglob("*.html"):
|
||||
rel = html.relative_to(out_dir)
|
||||
if rel.parts and rel.parts[0] in skip:
|
||||
continue
|
||||
text = html.read_text(encoding="utf-8")
|
||||
cur = rel.parent.as_posix()
|
||||
|
||||
def _href(local: str, anchor: str) -> str | None:
|
||||
target = page_paths.get(local)
|
||||
if not target:
|
||||
return None
|
||||
href = target if cur == "." else os.path.relpath(target, cur)
|
||||
return f'href="{href}{anchor}"'
|
||||
|
||||
def _ext(m: "re.Match") -> str:
|
||||
member = _DOXY_ANCHOR_TO_MEMBER.get(m.group("frag") or "")
|
||||
h = _href(_doxy_page_to_local(m.group("page")),
|
||||
f"#{member}" if member else "")
|
||||
if not h:
|
||||
return m.group(0)
|
||||
# Flip the now-local link from external to internal styling.
|
||||
return f'<a class="reference internal" {h}' if m.group("a") else h
|
||||
|
||||
def _bare(m: "re.Match") -> str:
|
||||
frag = m.group("frag")
|
||||
return _href(m.group("page"), f"#{frag}" if frag else "") or m.group(0)
|
||||
|
||||
def _inc(m: "re.Match") -> str:
|
||||
h = _href(m.group("file"), "")
|
||||
if not h:
|
||||
return m.group(0)
|
||||
return f'<a class="reference internal opencv-include-link" {h}>{m.group("path")}</a>'
|
||||
|
||||
new = inc_re.sub(_inc, bare_re.sub(_bare, ext_re.sub(_ext, text)))
|
||||
new = strip_re.sub(lambda m: m.group("txt"), new)
|
||||
if new != text:
|
||||
html.write_text(new, encoding="utf-8")
|
||||
|
||||
|
||||
def _drop_moved_stub_search_entries() -> None:
|
||||
"""Drop moved-tutorial stub pages from the Doxygen search index."""
|
||||
doxy_html = _API_XML_DIR.parent / "html"
|
||||
search_dir = doxy_html / "search"
|
||||
if not search_dir.is_dir():
|
||||
return
|
||||
stubs = set()
|
||||
for h in doxy_html.rglob("*table_of_content*.html"):
|
||||
try:
|
||||
if "has been moved to this page" in h.read_text(encoding="utf-8", errors="ignore"):
|
||||
stubs.add(h.name)
|
||||
except OSError:
|
||||
pass
|
||||
if not stubs:
|
||||
return
|
||||
for js in search_dir.glob("*.js"):
|
||||
lines = js.read_text(encoding="utf-8", errors="ignore").splitlines(keepends=True)
|
||||
# Drop only single-target stub entries; keep multi-target terms.
|
||||
kept = [ln for ln in lines if not (
|
||||
ln.count("['../") == 1 and any(f"/{s}'" in ln for s in stubs))]
|
||||
if len(kept) != len(lines):
|
||||
js.write_text("".join(kept), encoding="utf-8")
|
||||
|
||||
|
||||
# Pygments emits `<span class="n">NAME</span>` (and `class="nc"`/`"nf"`
|
||||
# for class/function tokens) inside its rendered `<pre>` for every C++
|
||||
# identifier in a code block. The example pages, snippet pages, tutorial
|
||||
# samples, and any `:::{code-block} cpp` fence all go through Pygments,
|
||||
# so by the time the HTML is written the code blocks have full syntax
|
||||
# colouring but ZERO clickable tokens — `Mat`, `InputArray`,
|
||||
# `getOptimalDFTSize`, etc. are inert text.
|
||||
#
|
||||
# This pass wraps each such span in an `<a class="reference internal"
|
||||
# href="…">` when the token resolves via `_LOCAL_CLASS_URL` /
|
||||
# `_LOCAL_TYPEDEF_URL`, mirroring what the API-stub renderer already
|
||||
# does for inline `<programlisting>`. Idempotent: skips spans already
|
||||
# inside an `<a>`.
|
||||
_PYG_IDENT_SPAN_RE = re.compile(
|
||||
r'(?P<prefix><span class="(?:n|nc|nf|nb|nv|na)">)'
|
||||
r'(?P<name>[A-Za-z_][A-Za-z0-9_]*)'
|
||||
r'(?P<suffix></span>)'
|
||||
)
|
||||
|
||||
# Pygments preprocessor-file-name span:
|
||||
# `<span class="cpf">"opencv2/core.hpp"</span>` (or <…>)
|
||||
# The quote characters are HTML-escaped (`"` / `<` / `>`).
|
||||
# Capture the inner path so we can wrap it in an `<a>` linking to the
|
||||
# local Doxygen file page (`_FILE_URL` map). The quote chars stay
|
||||
# outside the anchor — mirrors how the enum-detail `#include` line is
|
||||
# rendered (`#include <a href="…">opencv2/core.hpp</a>`).
|
||||
_PYG_CPF_SPAN_RE = re.compile(
|
||||
r'(?P<prefix><span class="cpf">)'
|
||||
r'(?P<openq>"|<)'
|
||||
r'(?P<path>[A-Za-z0-9_./+\-]+\.[A-Za-z0-9]+)'
|
||||
r'(?P<closeq>"|>)'
|
||||
r'(?P<suffix></span>)'
|
||||
)
|
||||
|
||||
|
||||
def _linkify_code_blocks(html_dir: pathlib.Path) -> None:
|
||||
"""Walk every `.html` under `html_dir` and turn known identifier
|
||||
tokens inside Pygments-rendered `<pre>` blocks into clickable
|
||||
anchors. The substitution is scoped to spans inside `<pre>` so we
|
||||
don't accidentally repaint inline `<code class="n">` chips in
|
||||
prose; the rule above already targets only Pygments span classes
|
||||
that Pygments uses inside its `<pre>` output."""
|
||||
if not (_LOCAL_CLASS_URL or _LOCAL_TYPEDEF_URL or _FILE_URL):
|
||||
return
|
||||
if not html_dir.is_dir():
|
||||
return
|
||||
import os
|
||||
|
||||
def _resolve(name: str) -> str | None:
|
||||
return _LOCAL_CLASS_URL.get(name) or _LOCAL_TYPEDEF_URL.get(name)
|
||||
|
||||
# `<pre>…</pre>` blocks only — keeps the substitution from touching
|
||||
# inline `<span class="n">` runs that may appear in other contexts.
|
||||
_PRE_BLOCK_RE = re.compile(r"<pre>(.*?)</pre>", re.DOTALL)
|
||||
|
||||
# Relative path from each rendered `.html` file's directory to the
|
||||
# Doxygen html tree (which sits alongside `docs_sphinx/html/` at
|
||||
# `doc/doxygen/html/`). Reused per file so paths render with the
|
||||
# right number of `../` segments regardless of subdir depth.
|
||||
_DOXY_ROOT = html_dir.parent.parent / "doc" / "doxygen" / "html"
|
||||
|
||||
def _doxy_rel(html_path: pathlib.Path, file_url: str) -> str:
|
||||
target = _DOXY_ROOT / file_url
|
||||
try:
|
||||
return os.path.relpath(target, start=html_path.parent)
|
||||
except ValueError:
|
||||
return f"../../../doc/doxygen/html/{file_url}"
|
||||
|
||||
def _wrap_span(m: re.Match) -> str:
|
||||
name = m.group("name")
|
||||
url = _resolve(name)
|
||||
if not url:
|
||||
return m.group(0)
|
||||
return (f'<a class="reference internal" href="{url}">'
|
||||
f'{m.group("prefix")}{name}{m.group("suffix")}</a>')
|
||||
|
||||
def _wrap_cpf(m: re.Match, current_html: pathlib.Path) -> str:
|
||||
path = m.group("path")
|
||||
# Only opencv headers — `<iostream>`, `<stdio.h>` are not in
|
||||
# `_FILE_URL` and stay plain.
|
||||
file_url = _FILE_URL.get(path)
|
||||
if not file_url:
|
||||
return m.group(0)
|
||||
href = _doxy_rel(current_html, file_url)
|
||||
# Keep the opening/closing quotes outside the `<a>` (so they
|
||||
# render as plain `"` / `<`/`>`), and put the link on just
|
||||
# the path text — same shape the enum-detail `#include` line
|
||||
# already uses elsewhere.
|
||||
return (f'{m.group("prefix")}{m.group("openq")}'
|
||||
f'<a class="reference external opencv-include-link" '
|
||||
f'href="{href}">{path}</a>'
|
||||
f'{m.group("closeq")}{m.group("suffix")}')
|
||||
|
||||
def _rewrite_pre(m: re.Match, current_html: pathlib.Path) -> str:
|
||||
inner = m.group(1)
|
||||
# Skip spans already wrapped: if `<a …>` immediately precedes
|
||||
# the `<span class="n">…</span>`, leave it. The Pygments
|
||||
# output doesn't generate `<a>` itself, so the only place
|
||||
# `<a>` appears in the inner text is from a prior pass — we
|
||||
# detect by scanning for `<a `/`</a>` pairs and only rewrite
|
||||
# text outside them.
|
||||
out: list[str] = []
|
||||
i, n = 0, len(inner)
|
||||
while i < n:
|
||||
if inner.startswith("<a ", i):
|
||||
j = inner.find("</a>", i)
|
||||
if j < 0:
|
||||
out.append(inner[i:])
|
||||
break
|
||||
out.append(inner[i:j + 4])
|
||||
i = j + 4
|
||||
else:
|
||||
k = inner.find("<a ", i)
|
||||
if k < 0:
|
||||
seg = inner[i:]
|
||||
seg = _PYG_IDENT_SPAN_RE.sub(_wrap_span, seg)
|
||||
seg = _PYG_CPF_SPAN_RE.sub(
|
||||
lambda mm: _wrap_cpf(mm, current_html), seg)
|
||||
out.append(seg)
|
||||
break
|
||||
seg = inner[i:k]
|
||||
seg = _PYG_IDENT_SPAN_RE.sub(_wrap_span, seg)
|
||||
seg = _PYG_CPF_SPAN_RE.sub(
|
||||
lambda mm: _wrap_cpf(mm, current_html), seg)
|
||||
out.append(seg)
|
||||
i = k
|
||||
return "<pre>" + "".join(out) + "</pre>"
|
||||
|
||||
for html in html_dir.rglob("*.html"):
|
||||
try:
|
||||
text = html.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
if "<pre>" not in text:
|
||||
continue
|
||||
new_text = _PRE_BLOCK_RE.sub(
|
||||
lambda m: _rewrite_pre(m, html), text)
|
||||
if new_text != text:
|
||||
try:
|
||||
html.write_text(new_text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_INLINE_CODE_RE = re.compile(
|
||||
r'<code class="docutils literal notranslate">(?P<body>[^<]*?)</code>'
|
||||
)
|
||||
_A_BLOCK_RE = re.compile(r'<a\b[^>]*>.*?</a>', re.DOTALL)
|
||||
_INLINE_TOK_RE = re.compile(r'(?:cv::)?_?[A-Za-z][A-Za-z0-9_]*')
|
||||
|
||||
|
||||
def _linkify_inline_code(html_dir: pathlib.Path) -> None:
|
||||
if not (_LOCAL_CLASS_URL or _LOCAL_TYPEDEF_URL):
|
||||
return
|
||||
if not html_dir.is_dir():
|
||||
return
|
||||
|
||||
def _resolve(name: str) -> str | None:
|
||||
bare = name[4:] if name.startswith("cv::") else name
|
||||
return _LOCAL_CLASS_URL.get(bare) or _LOCAL_TYPEDEF_URL.get(bare)
|
||||
|
||||
def _anchor_text(tok: str) -> str:
|
||||
return tok.replace("::", "::")
|
||||
|
||||
def _linkify_body(body: str) -> str:
|
||||
def _sub(m: re.Match) -> str:
|
||||
tok = m.group(0)
|
||||
url = _resolve(tok)
|
||||
if not url:
|
||||
return tok
|
||||
return (f'<a class="reference internal" '
|
||||
f'href="{url}">{_anchor_text(tok)}</a>')
|
||||
return _INLINE_TOK_RE.sub(_sub, body)
|
||||
|
||||
def _rewrite_code(m: re.Match) -> str:
|
||||
body = m.group("body")
|
||||
new_body = _linkify_body(body)
|
||||
if new_body == body:
|
||||
return m.group(0)
|
||||
return (f'<code class="docutils literal notranslate">'
|
||||
f'{new_body}</code>')
|
||||
|
||||
for html in html_dir.rglob("*.html"):
|
||||
try:
|
||||
text = html.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
if 'class="docutils literal notranslate"' not in text:
|
||||
continue
|
||||
masked: list[str] = []
|
||||
def _mask(m: re.Match) -> str:
|
||||
masked.append(m.group(0))
|
||||
return f"\x00A{len(masked) - 1}\x00"
|
||||
masked_text = _A_BLOCK_RE.sub(_mask, text)
|
||||
new_masked = _INLINE_CODE_RE.sub(_rewrite_code, masked_text)
|
||||
if new_masked == masked_text:
|
||||
continue
|
||||
new_text = re.sub(
|
||||
r"\x00A(\d+)\x00",
|
||||
lambda mm: masked[int(mm.group(1))],
|
||||
new_masked,
|
||||
)
|
||||
try:
|
||||
html.write_text(new_text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _inline_coll_graphs_on_finish(app, exception):
|
||||
"""build-finished entry point."""
|
||||
if exception is not None:
|
||||
return
|
||||
out = pathlib.Path(app.outdir)
|
||||
for _api in ("main_modules", "extra_modules"):
|
||||
_inline_collaboration_svgs(out / _api, out / "_images")
|
||||
_strip_breathe_class_clutter(out / _api)
|
||||
_linkify_code_blocks(out)
|
||||
_linkify_inline_code(out)
|
||||
_localize_doxygen_links(out)
|
||||
_drop_moved_stub_search_entries()
|
||||
_copy_js_tryit_files(out)
|
||||
_fix_gapi_images(out)
|
||||
_generate_search_map(out)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user