[^)]+)\)',
_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(
# Block a markdown-link URL via a 2-char lookbehind on `](`, instead of
# excluding a bare `(` — so a URL in plain parens, e.g.
# `PyPI (https://pypi.org/...)`, still gets autolinkified while
# markdown-link/autolink/HTML-attribute exclusions stay intact.
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]")
# Per-line fence detector for `_code_regions`. Deliberately allows ANY
# leading whitespace (not CommonMark's ≤3): our pipeline emits fences at
# the 4+-space indent of their originating `@include`/`@snippet` inside an
# `@add_toggle`, and under the strict limit those slipped past, leaking
# `[cv.foo](#anchor)` into code blocks. Worst case of the relaxation is
# MORE text protected from the linkifiers, not less.
_FENCE_LINE_RE = re.compile(
r"^(?P[ \t]*)(?P`{3,}|~{3,})(?P.*)$"
)
def _code_regions(src: str) -> list[tuple[int, int]]:
"""Return sorted `[(start, end), ...]` byte ranges spanning every fenced
code block (fence lines included) in `src`.
Handles MyST/Sphinx nested fences where the outer container is wider than
the inner code fence (e.g. `{tab-set}` (6) > `{tab-item}` (5) >
` ```python` (3)). Only *code*-block ranges are returned; a `{directive}`
info string marks a container, whose body the caller still walks so its
nested prose stays linkifiable. Inside a code fence, further fence-shaped
lines are inert per CommonMark §4.5."""
out: list[tuple[int, int]] = []
# Each stack entry: (fence_char, fence_width, is_code, opener_offset).
stack: list[tuple[str, int, bool, int]] = []
pos = 0
for line in src.splitlines(keepends=True):
line_start = pos
next_pos = pos + len(line)
ln = line.rstrip("\n").rstrip("\r")
m = _FENCE_LINE_RE.match(ln)
if m is None:
pos = next_pos
continue
fc = m.group("fence")[0]
fw = len(m.group("fence"))
info = m.group("info").strip()
# Inside a code fence nothing matters except the matching closer.
if stack and stack[-1][2]:
top_char, top_width, _, opener_start = stack[-1]
if fc == top_char and fw >= top_width and not info:
out.append((opener_start, next_pos))
stack.pop()
pos = next_pos
continue
# Outside code: an info-less fence of the same char & ≥ width closes
# the nearest open fence (a container, per the branch above); a fence
# with info opens a new block.
if stack and not info:
top_char, top_width, _, _ = stack[-1]
if fc == top_char and fw >= top_width:
stack.pop()
pos = next_pos
continue
# Opener: `{…}` info → container, anything else (language tag, plain
# text, or empty) → code block per CommonMark.
is_code = (not info) or (not info.startswith("{"))
stack.append((fc, fw, is_code, line_start))
# Shield a breathe directive's opener line: its argument (e.g.
# `{doxygenstruct} cv::MSTEdge`) must reach breathe unlinkified.
if info.startswith("{doxygen"):
out.append((line_start, next_pos))
pos = next_pos
# Unclosed code fences: extend protection to EOF so we don't mangle
# the tail of a pathologically-truncated document.
while stack:
char, width, is_code, opener_start = stack.pop()
if is_code:
out.append((opener_start, len(src)))
out.sort()
return out
def _apply_outside_code(src: str, transform) -> str:
"""Apply `transform` to regions outside fenced and inline code, using
`_code_regions` so nested container > code fences (tab-set > tab-item >
` ```python`) are excluded correctly."""
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 s, e in _code_regions(src):
out.append(_segment(src[last:s]))
out.append(src[s:e])
last = e
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