[^)]+)\)',
_img_repl, text)
# 12b. Bare image filename (no dir prefix, e.g. "shape.jpg") -> _IMAGE_INDEX lookup.
def _bare_img_repl(m: re.Match) -> str:
rel = m.group("rel")
if docname:
local = DOC_ROOT / pathlib.Path(docname).parent / "images" / rel
if local.is_file():
return f'{m.group("pre")}images/{rel})'
hit = _IMAGE_INDEX.get(rel)
return f'{m.group("pre")}/{hit})' if hit else m.group(0)
text = re.sub(
r'(?P!\[[^\]]*\]\()(?P[A-Za-z0-9_.-]+\.[A-Za-z]{2,4})\)',
_bare_img_repl, text)
# 12c. Cross-tree contrib image refs -> raw-HTML
, depth-relative URL.
def _img_xtree(m: re.Match) -> str:
alt, rel = m.group("alt"), m.group("rel")
if rel.startswith("/") or "://" in rel:
return m.group(0)
if rel.startswith("./"):
rel = rel[2:]
if not docname or not docname.startswith("tutorials_contrib/"):
return m.group(0)
parts = pathlib.Path(docname).parent.parts
if len(parts) < 2:
return m.group(0)
module = parts[1]
for cand in (f"{module}/doc/{rel}",
f"{module}/{rel}",
rel):
if (CONTRIB_ROOT / cand).is_file():
return _emit_contrib_img(cand, alt)
return m.group(0)
text = re.sub(
r'!\[(?P[^\]]*)\]\((?P[^)]+)\)',
_img_xtree, text)
# 12d. Blank line between consecutive `Label: ` lines (skip table rows).
text = re.sub(
r"^(?P(?!\|)[^\n]*!\[[^\]]*\]\([^)]+\)[^\n]*)\n"
r"(?=(?!\|)[^\n]*!\[[^\]]*\]\([^)]+\))",
r"\g\n\n", text, flags=re.MULTILINE)
# 12e. `` -> MyST `{figure}`.
text = re.sub(
r"^(?P[ \t]*)!\[(?PFigure\s[^\]]+)\]\((?P[^)]+)\)\s*$",
lambda m: (f"{m.group('indent')}:::{{figure}} {m.group('url')}\n"
f"{m.group('indent')}{m.group('caption')}\n"
f"{m.group('indent')}:::"),
text, flags=re.MULTILINE)
# 13. Wrap front-matter table in a `.opencv-meta-table` div.
def _wrap_front_matter(src: str) -> str:
pat = re.compile(
r"(^\|[^\n]*\|[ \t]*\n"
r"\|[ \t]*-:[ \t]*\|[ \t]*:-[ \t]*\|[ \t]*\n"
r"(?:\|[^\n]*\|[ \t]*\n)+)",
re.MULTILINE)
def repl(m: re.Match) -> str:
return f":::{{div}} opencv-meta-table\n\n{m.group(1)}\n:::\n"
return pat.sub(repl, src, count=1)
text = _wrap_front_matter(text)
# 14a. Auto-link `cv.SymbolName` in prose.
text = _linkify_cv_symbols(text)
# 14b. Wrap bare URLs in `<...>` (runs after 14a).
text = _linkify_bare_urls(text)
# 14c. Resolve `opencv_source_code/` Doxygen alias -> GitHub link.
text = _linkify_opencv_source_code(text)
# 14d. Resolve Doxygen `#funcName` cross-references in prose -> link.
text = _linkify_dox_hash_refs(text)
# 15. Restore @verbatim stash (see step 0v).
for _vk, _vv in _verbatim_stash.items():
text = text.replace(_vk, _vv)
return text
_BARE_URL_RE = re.compile(
r"(?https?://[^\s<>()`\"']+[^\s<>()`\"'.,;:!?])"
)
# `cv.X`/`cv::X`; lookbehind blocks `frame.cv.X`-style false positives.
_CV_SYMBOL_RE = re.compile(
r"(?cv(?:\.|::))(?P[A-Za-z_]\w*)(?P\(\))?"
)
# Bare `funcName()` (Doxygen auto-linked these).
_BARE_FN_RE = re.compile(r"(?\"])(?P[A-Za-z_]\w{2,})\(\)")
_FENCED_BLOCK_RE = re.compile(
r"^(?P[`~]{3,})[^\n]*\n[\s\S]*?\n(?P=fence)[ \t]*$",
re.MULTILINE,
)
_INLINE_CODE_RE = re.compile(r"`+[^`\n]*?`+")
# ATX heading line; exempted from the auto-linkifier.
_ATX_HEADING_RE = re.compile(r"^[ \t]{0,3}#{1,6}[ \t]")
def _apply_outside_code(src: str, transform) -> str:
"""Apply `transform` to regions outside fenced/inline code."""
def _segment(text: str) -> str:
out, last = [], 0
for cm in _INLINE_CODE_RE.finditer(text):
out.append(transform(text[last:cm.start()]))
out.append(cm.group(0))
last = cm.end()
out.append(transform(text[last:]))
return "".join(out)
out, last = [], 0
for fm in _FENCED_BLOCK_RE.finditer(src):
out.append(_segment(src[last:fm.start()]))
out.append(fm.group(0))
last = fm.end()
out.append(_segment(src[last:]))
return "".join(out)
def _linkify_bare_urls(src: str) -> str:
return _apply_outside_code(src,
lambda chunk: _BARE_URL_RE.sub(r"<\g>", chunk))
_OPENCV_SOURCE_CODE_RE = re.compile(
r"\bopencv_source_code/(?P[\w./\-]+)"
)
def _linkify_opencv_source_code(src: str) -> str:
def _repl(m: re.Match) -> str:
path = m.group("path")
url = f"https://github.com/opencv/opencv/blob/5.x/{path}"
return f"[opencv_source_code/{path}]({url})"
return _apply_outside_code(
src, lambda chunk: _OPENCV_SOURCE_CODE_RE.sub(_repl, chunk))
_DOX_HASH_REF_RE = re.compile(
r"(?[A-Za-z_]\w*)\b"
)
def _linkify_dox_hash_refs(src: str) -> str:
if not _CV_SYMBOL_URL:
return src
def _repl(m: re.Match) -> str:
name = m.group("name")
url = _CV_SYMBOL_URL.get(name)
if not url:
return m.group(0)
return f"[{name}]({url})"
return _apply_outside_code(
src, lambda chunk: _DOX_HASH_REF_RE.sub(_repl, chunk))
def _linkify_cv_symbols(src: str) -> str:
if not _CV_SYMBOL_URL:
return src
def _local_url(sym: str) -> str | None:
"""Prefer a Sphinx-local URL when one exists for `sym`.
`_LOCAL_CLASS_URL` and `_LOCAL_TYPEDEF_URL` are populated from
the tagfile but rewritten to point at the local api/ tree
(e.g. `classcv_1_1Mat.html`, `core_basic.html#vec2b`). If no
local entry exists, return None so the caller drops the link —
we never bounce readers off-site for symbols whose Sphinx
version isn't present in this build.
"""
return _LOCAL_CLASS_URL.get(sym) or _LOCAL_TYPEDEF_URL.get(sym)
def repl_cv(m: re.Match) -> str:
sym = m.group("sym")
url = _local_url(sym)
if not url:
return m.group(0)
sep = m.group("sep")
parens = m.group("parens") or ""
return f'{sep}{sym}{parens}'
def repl_bare(m: re.Match) -> str:
sym = m.group("sym")
url = _local_url(sym)
if not url:
return m.group(0)
return f'{sym}()'
def transform(chunk: str) -> str:
# Headings are definitions — don't linkify.
out_lines = []
for line in chunk.split("\n"):
if _ATX_HEADING_RE.match(line):
out_lines.append(line)
continue
line = _CV_SYMBOL_RE.sub(repl_cv, line)
line = _BARE_FN_RE.sub(repl_bare, line)
out_lines.append(line)
return "\n".join(out_lines)
return _apply_outside_code(src, transform)
_referenced_docs_cache: set[str] | None = None
def _referenced_docs() -> set[str]:
"""Docnames reachable from some @subpage/@ref (computed once)."""
global _referenced_docs_cache
if _referenced_docs_cache is None:
_referenced_docs_cache = {
_ANCHOR_TO_DOC[a] for a in _REFERENCED_ANCHORS
if a in _ANCHOR_TO_DOC
}
return _referenced_docs_cache
_TUTORIAL_PREFIXES = ("tutorials/", "js_tutorials/",
"py_tutorials/", "tutorials_contrib/")
def _source_read(app, docname, source):
# Translate tutorial docs + API stubs; doxygengroup bodies pass through.
if not (docname.startswith("tutorials/")
or docname.startswith("js_tutorials/")
or docname.startswith("py_tutorials/")
or docname.startswith("tutorials_contrib/")
or docname.startswith("main_modules/")
or docname.startswith("extra_modules/")
or docname == "faq"
or docname == "citelist"
or docname == "intro"
or docname == "index"):
return
text = source[0]
# Master doc: append contrib/api roots without editing tutorials.markdown.
if docname == "tutorials/tutorials" and not USE_INDEX_LANDING:
if CONTRIB_MODULES and "tutorial_contrib_root" in _ANCHOR_TO_DOC:
text = text.rstrip() + "\n\n- @subpage tutorial_contrib_root\n"
if API_MODULES and "api_root" in _ANCHOR_TO_DOC:
text = text.rstrip() + "\n\n- @subpage api_root\n"
out = _translate(text, docname)
# Mark unreferenced tutorial pages :orphan: (skip master/front-matter/referenced).
if (docname.startswith(_TUTORIAL_PREFIXES)
and docname != "tutorials/tutorials"
and docname not in _referenced_docs()
and not out.lstrip().startswith("---")):
out = "---\norphan: true\n---\n\n" + out
source[0] = out