# 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'/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 /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 = ['
'] for heading, link_text, docname in entries: if link_text is None: html_lines.append( f'

{heading}

') else: html_lines.append(f'

{heading}

') html_lines.append(f'

{link_text}

') html_lines.append("
") 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'
  • {_t}
  • ' 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'\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'
  • {_d}
  • ' 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'\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
  • 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 = ["
      "] for _part in sorted(node, key=str.lower): child = node[_part] info = _ALL_NAMESPACES.get(child["_full"]) if info: label = f'{_part}' if info.get("brief"): label += f' — {_esc(info["brief"])}' else: label = _part out.append(f"
    • {label}") if child["_kids"]: out += render(child["_kids"]) out.append("
    • ") out.append("
    ") 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'
    \n{chr(10).join(render(tree))}\n
    \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 = ['
      '] 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'{ns}' body.append(f"
    • {heading}") body.append("
        ") for leaf, info in sorted(by_ns[ns], key=lambda t: t[0].lower()): entry = f'{leaf}' if info.get("brief"): entry += f' — {_esc(info["brief"])}' body.append(f"
      • {entry}
      • ") body.append("
    • ") body.append("
    ") text = ( "---\norphan: true\n---\n" "# Class List\n\n" "Here are the classes, structs and unions with brief descriptions.\n\n" f'
    \n{chr(10).join(body)}\n
    \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)