mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
Merge pull request #29091 from abhishek-gola:doc_optimizations
New sphinx-documentation build #29091 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
@@ -1040,6 +1040,7 @@ ocv_register_modules()
|
||||
|
||||
# Generate targets for documentation
|
||||
add_subdirectory(doc)
|
||||
add_subdirectory(docs_sphinx)
|
||||
|
||||
# various data that is used by cv libraries and/or demo applications.
|
||||
# add_subdirectory(data)
|
||||
@@ -1310,6 +1311,7 @@ ocv_build_features_string(docs_status
|
||||
IF TARGET doxygen_cpp THEN "doxygen"
|
||||
IF TARGET doxygen_python THEN "python"
|
||||
IF TARGET doxygen_javadoc THEN "javadoc"
|
||||
IF TARGET sphinx THEN "sphinx"
|
||||
IF BUILD_opencv_js OR DEFINED OPENCV_JS_LOCATION THEN "js"
|
||||
ELSE "NO"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Sphinx wrapper for the OpenCV doc/ tree.
|
||||
# Adds a `sphinx` custom target so `cmake --build <build> --target sphinx`
|
||||
# works the same way `--target doxygen` does.
|
||||
#
|
||||
# Gated on BUILD_DOCS so it follows the existing toggle.
|
||||
|
||||
if(NOT BUILD_DOCS)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Prefer the sphinx-build that lives in the conda env where myst-parser /
|
||||
# pydata-sphinx-theme / breathe / exhale are installed. Override on the
|
||||
# CMake command line: -DSPHINX_BUILD=/path/to/sphinx-build
|
||||
find_program(SPHINX_BUILD
|
||||
NAMES sphinx-build
|
||||
DOC "Path to sphinx-build tool")
|
||||
|
||||
if(NOT SPHINX_BUILD)
|
||||
message(STATUS "docs_sphinx: sphinx-build not found; `sphinx` target disabled")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_SPHINX_SOURCE "${CMAKE_SOURCE_DIR}/doc")
|
||||
set(_SPHINX_CONFDIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(_SPHINX_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/html")
|
||||
|
||||
add_custom_target(sphinx
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${_SPHINX_OUTDIR}
|
||||
COMMAND ${SPHINX_BUILD} -c ${_SPHINX_CONFDIR} ${_SPHINX_SOURCE} ${_SPHINX_OUTDIR}
|
||||
WORKING_DIRECTORY ${_SPHINX_CONFDIR}
|
||||
COMMENT "Building Sphinx HTML site -> ${_SPHINX_OUTDIR}"
|
||||
VERBATIM)
|
||||
|
||||
# Clean rebuild: `cmake --build <build> --target sphinx-clean`
|
||||
add_custom_target(sphinx-clean
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${_SPHINX_OUTDIR}
|
||||
COMMENT "Removing ${_SPHINX_OUTDIR}"
|
||||
VERBATIM)
|
||||
|
||||
message(STATUS "docs_sphinx: sphinx target enabled (sphinx-build: ${SPHINX_BUILD})")
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* docs_sphinx/_static/custom.css
|
||||
* Ported visual layer from the Prasadayus/opencv#27 Sphinx port,
|
||||
* trimmed to the rules that apply to our minimal wrapper (PyData
|
||||
* pydata-sphinx-theme + MyST, no doxysnippet/opencv_code_links/tabs
|
||||
* extensions). Rules that target HTML markup we don't emit are
|
||||
* dropped; rules that are pure pydata overrides are kept verbatim.
|
||||
*/
|
||||
|
||||
:root,
|
||||
html[data-theme="light"],
|
||||
html[data-theme="dark"] {
|
||||
--pst-font-family-base: "Inter", "Source Sans Pro", -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
sans-serif;
|
||||
--pst-font-family-heading: var(--pst-font-family-base);
|
||||
--pst-font-family-monospace: "JetBrains Mono", "SFMono-Regular", Menlo,
|
||||
Consolas, "Liberation Mono", monospace;
|
||||
--pst-font-size-base: 16px;
|
||||
}
|
||||
|
||||
html { font-size: 16px; }
|
||||
body, .bd-main { font-family: var(--pst-font-family-base); }
|
||||
|
||||
h1 { font-size: 2.25rem; font-weight: 700; line-height: 1.2; letter-spacing: -0.02em; }
|
||||
h2 { font-size: 1.5rem; font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; margin-top: 2rem; }
|
||||
h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.4; }
|
||||
|
||||
.bd-content p, .bd-content li { line-height: 1.6; }
|
||||
.bd-content ol > li { margin-bottom: 1.1rem; padding-left: 0.25rem; }
|
||||
.bd-content ol ol { list-style-type: lower-alpha; }
|
||||
.bd-content ol ol ol { list-style-type: lower-roman; }
|
||||
.bd-content ol > li > p:first-child > strong:first-child::after { content: ":"; }
|
||||
|
||||
html[data-theme="light"] {
|
||||
--pst-color-link: #0969da;
|
||||
--pst-color-link-hover: #0550ae;
|
||||
--opencv-accent: #003a6b;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
--opencv-accent: #a4c9ff;
|
||||
}
|
||||
|
||||
.bd-content a { text-decoration: none; color: #0969da; }
|
||||
html[data-theme="dark"] .bd-article-container a { color: #539bf5 !important; }
|
||||
.bd-content a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.22em;
|
||||
text-decoration-thickness: 1px;
|
||||
color: #0550ae;
|
||||
}
|
||||
html[data-theme="dark"] .bd-article-container a:hover { color: #6cb6ff !important; }
|
||||
.bd-content a > code, .bd-content code > a { color: inherit; }
|
||||
|
||||
/* --- Front-matter table (.opencv-meta-table — applied in source-read) -- */
|
||||
.opencv-meta-table,
|
||||
.opencv-meta-table .pst-scrollable-table-container { margin: 0 !important; padding: 0 !important; }
|
||||
.opencv-meta-table table { margin-bottom: 0 !important; }
|
||||
.opencv-meta-table + p,
|
||||
div.opencv-meta-table + p { margin-top: 1.5rem !important; }
|
||||
.opencv-meta-table .pst-scrollable-table-container,
|
||||
.opencv-meta-table table,
|
||||
table.opencv-meta-table {
|
||||
width: auto !important;
|
||||
max-width: fit-content;
|
||||
margin-right: auto;
|
||||
margin-left: 0;
|
||||
border: 1px solid var(--pst-color-border);
|
||||
border-radius: 0.5rem;
|
||||
border-collapse: separate !important;
|
||||
border-spacing: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
.opencv-meta-table table th,
|
||||
.opencv-meta-table table td,
|
||||
table.opencv-meta-table th,
|
||||
table.opencv-meta-table td {
|
||||
padding: 0.45rem 1rem;
|
||||
font-size: 0.92rem;
|
||||
border-bottom: 1px solid var(--pst-color-border);
|
||||
border-right: 1px solid var(--pst-color-border);
|
||||
vertical-align: middle;
|
||||
text-align: left !important;
|
||||
}
|
||||
.opencv-meta-table table td:first-child,
|
||||
table.opencv-meta-table td:first-child {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: var(--pst-color-on-surface);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opencv-meta-table table tr:last-child td,
|
||||
table.opencv-meta-table tr:last-child td { border-bottom: none; }
|
||||
.opencv-meta-table table td:last-child,
|
||||
table.opencv-meta-table td:last-child { border-right: none; }
|
||||
.opencv-meta-table table thead,
|
||||
table.opencv-meta-table thead { display: none; }
|
||||
|
||||
html[data-theme="dark"] .opencv-meta-table table,
|
||||
html[data-theme="dark"] table.opencv-meta-table {
|
||||
border: 1px solid #444c56 !important;
|
||||
border-radius: 0.5rem !important;
|
||||
overflow: hidden !important;
|
||||
border-collapse: separate !important;
|
||||
border-spacing: 0 !important;
|
||||
}
|
||||
html[data-theme="dark"] .opencv-meta-table table th,
|
||||
html[data-theme="dark"] .opencv-meta-table table td,
|
||||
html[data-theme="dark"] table.opencv-meta-table th,
|
||||
html[data-theme="dark"] table.opencv-meta-table td {
|
||||
border-bottom: 1px solid #444c56 !important;
|
||||
border-right: 1px solid #444c56 !important;
|
||||
color: #cdd9e5 !important;
|
||||
background-color: #22272e !important;
|
||||
}
|
||||
html[data-theme="dark"] .opencv-meta-table table td:first-child,
|
||||
html[data-theme="dark"] table.opencv-meta-table td:first-child {
|
||||
background: #2d333b !important;
|
||||
font-weight: 600 !important;
|
||||
color: #cdd9e5 !important;
|
||||
}
|
||||
html[data-theme="dark"] .opencv-meta-table table tr:last-child td,
|
||||
html[data-theme="dark"] table.opencv-meta-table tr:last-child td { border-bottom: none !important; }
|
||||
html[data-theme="dark"] .opencv-meta-table table td:last-child,
|
||||
html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: none !important; }
|
||||
|
||||
/* --- Images & figures -------------------------------------------------- */
|
||||
.bd-content figure figcaption,
|
||||
.bd-content figure figcaption p,
|
||||
.bd-content figure figcaption .caption-text {
|
||||
font-weight: 700 !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
.bd-content figure,
|
||||
.bd-content p img,
|
||||
.bd-content li img { margin-top: 1rem; margin-bottom: 1rem; }
|
||||
.bd-content figure { margin-left: auto; margin-right: auto; text-align: center; }
|
||||
.bd-content p:has(> img:only-child) { text-align: center; }
|
||||
.bd-content p > img { display: inline-block; }
|
||||
|
||||
/* --- YouTube embeds (.opencv-youtube — emitted by @youtube{ID} rule) --- */
|
||||
.opencv-youtube {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 1.5rem auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.opencv-youtube iframe {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* --- Generic tables ---------------------------------------------------- */
|
||||
.bd-content table {
|
||||
border-collapse: collapse !important;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--pst-color-border) !important;
|
||||
}
|
||||
.bd-content table th {
|
||||
border: 1px solid var(--pst-color-border) !important;
|
||||
padding: 0.65rem 1rem !important;
|
||||
background: var(--pst-color-surface) !important;
|
||||
font-size: 0.8rem !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.07em !important;
|
||||
color: var(--pst-color-muted) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.bd-content table td {
|
||||
border: 1px solid var(--pst-color-border) !important;
|
||||
padding: 0.65rem 1rem !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
.bd-content table tbody tr:hover td { background: var(--pst-color-surface) !important; }
|
||||
|
||||
/* --- Header & navbar --------------------------------------------------- */
|
||||
.bd-header {
|
||||
border-bottom: 1px solid var(--pst-color-border);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
.bd-header .navbar { min-height: 3.25rem; padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
||||
.bd-header .navbar-header-items__start { gap: 1rem; align-items: center; }
|
||||
.bd-header img.logo__image { max-height: 36px; }
|
||||
|
||||
.version-badge {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.5rem;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--pst-color-text-muted);
|
||||
background: var(--pst-color-surface);
|
||||
border: 1px solid var(--pst-color-border);
|
||||
border-radius: 0.35rem;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.bd-header .navbar-nav { gap: 0.25rem; }
|
||||
.bd-header .navbar-nav > li > a {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--pst-color-text-muted);
|
||||
padding: 0.4rem 0.75rem !important;
|
||||
border-radius: 0.35rem;
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
}
|
||||
.bd-header .navbar-nav > li > a:hover {
|
||||
color: var(--opencv-accent);
|
||||
background: var(--pst-color-surface);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bd-header .navbar-nav > li > a.active,
|
||||
.bd-header .navbar-nav > li > a.current {
|
||||
color: var(--opencv-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.bd-header .navbar-persistent--mobile,
|
||||
.bd-header .navbar-persistent--container,
|
||||
.bd-header button.theme-switch-button,
|
||||
.bd-header .navbar-icon-links a.nav-link { color: var(--pst-color-text-muted); }
|
||||
.bd-header button.theme-switch-button:hover,
|
||||
.bd-header .navbar-icon-links a.nav-link:hover { color: var(--opencv-accent); }
|
||||
|
||||
/* --- Wider reading column ---------------------------------------------- */
|
||||
@media (min-width: 960px) {
|
||||
.bd-page-width,
|
||||
.bd-container__inner.bd-page-width { max-width: none !important; }
|
||||
.bd-container { max-width: none !important; }
|
||||
.bd-sidebar-primary { padding-left: 1rem !important; padding-right: 1rem !important; }
|
||||
.bd-main .bd-content,
|
||||
.bd-main .bd-article-container { padding-left: 2rem; padding-right: 2rem; }
|
||||
.bd-main .bd-article { padding-left: 0; padding-right: 0; }
|
||||
}
|
||||
|
||||
/* --- Left sidebar nav -------------------------------------------------- */
|
||||
.bd-sidebar-primary nav.bd-links { margin-right: 0 !important; }
|
||||
.bd-sidebar-primary nav.bd-docs-nav p.bd-links__title {
|
||||
font-size: 0.9rem !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: none !important;
|
||||
letter-spacing: normal !important;
|
||||
color: var(--pst-color-text-base) !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.bd-sidebar-primary nav.bd-links li > a {
|
||||
font-size: 0.9rem !important;
|
||||
line-height: 1.45 !important;
|
||||
padding-top: 0.28rem !important;
|
||||
padding-bottom: 0.28rem !important;
|
||||
}
|
||||
.bd-sidebar-primary nav.bd-links .toctree-l1 > a {
|
||||
font-weight: 500 !important;
|
||||
padding-left: 0.65rem !important;
|
||||
}
|
||||
.bd-sidebar-primary nav.bd-links .toctree-l2 > a {
|
||||
font-size: 0.875rem !important;
|
||||
padding-left: 1.25rem !important;
|
||||
color: var(--pst-color-text-muted) !important;
|
||||
}
|
||||
.bd-sidebar-primary nav.bd-links .toctree-l3 > a {
|
||||
font-size: 0.86rem !important;
|
||||
padding-left: 2rem !important;
|
||||
color: var(--pst-color-text-muted) !important;
|
||||
}
|
||||
.bd-sidebar-primary nav.bd-links .current > a {
|
||||
color: var(--pst-color-primary) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
html[data-theme="dark"] .bd-sidebar-primary nav.bd-links .current > a { color: #539bf5 !important; }
|
||||
|
||||
/* --- Code blocks ------------------------------------------------------- */
|
||||
div.highlight {
|
||||
position: relative;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
div.highlight pre {
|
||||
font-size: 0.875rem !important;
|
||||
line-height: 1.55 !important;
|
||||
overflow-x: auto !important;
|
||||
white-space: pre !important;
|
||||
background: #f6f8fa !important;
|
||||
color: #24292e !important;
|
||||
border: none !important;
|
||||
border-left: 3px solid #0550ae !important;
|
||||
border-radius: 0.4rem !important;
|
||||
padding: 0.6rem 0.85rem !important;
|
||||
margin: 0 !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
.highlight .c, .highlight .ch, .highlight .cm,
|
||||
.highlight .cpf, .highlight .c1, .highlight .cs { font-style: normal !important; }
|
||||
|
||||
html[data-theme="dark"] { color-scheme: dark; }
|
||||
html[data-theme="dark"] div.highlight span,
|
||||
html[data-theme="dark"] div.highlight * { color: unset !important; }
|
||||
|
||||
/* Dark-mode Pygments tokens (matches the PR's mapping) */
|
||||
html[data-theme="dark"] .highlight .c, html[data-theme="dark"] .highlight .c1,
|
||||
html[data-theme="dark"] .highlight .c2, html[data-theme="dark"] .highlight .cm,
|
||||
html[data-theme="dark"] .highlight .cp, html[data-theme="dark"] .highlight .cs,
|
||||
html[data-theme="dark"] .highlight .ch,
|
||||
html[data-theme="dark"] .highlight .cpf { color: #e3b341 !important; font-style: normal !important; }
|
||||
html[data-theme="dark"] .highlight .k, html[data-theme="dark"] .highlight .kd,
|
||||
html[data-theme="dark"] .highlight .kn, html[data-theme="dark"] .highlight .kp,
|
||||
html[data-theme="dark"] .highlight .kr,
|
||||
html[data-theme="dark"] .highlight .kt { color: #f47067 !important; font-weight: 600; }
|
||||
html[data-theme="dark"] .highlight .s, html[data-theme="dark"] .highlight .s1,
|
||||
html[data-theme="dark"] .highlight .s2, html[data-theme="dark"] .highlight .sa,
|
||||
html[data-theme="dark"] .highlight .sb, html[data-theme="dark"] .highlight .sc,
|
||||
html[data-theme="dark"] .highlight .sd, html[data-theme="dark"] .highlight .se,
|
||||
html[data-theme="dark"] .highlight .si,
|
||||
html[data-theme="dark"] .highlight .ss { color: #96d0ff !important; }
|
||||
html[data-theme="dark"] .highlight .n, html[data-theme="dark"] .highlight .na,
|
||||
html[data-theme="dark"] .highlight .nb, html[data-theme="dark"] .highlight .nc,
|
||||
html[data-theme="dark"] .highlight .nf,
|
||||
html[data-theme="dark"] .highlight .nn { color: #cdd9e5 !important; }
|
||||
html[data-theme="dark"] .highlight .o,
|
||||
html[data-theme="dark"] .highlight .ow { color: #f47067 !important; }
|
||||
html[data-theme="dark"] .highlight .mi, html[data-theme="dark"] .highlight .mf,
|
||||
html[data-theme="dark"] .highlight .mh,
|
||||
html[data-theme="dark"] .highlight .mo { color: #6cb6ff !important; }
|
||||
|
||||
html[data-theme="dark"] div.highlight pre {
|
||||
background: #2d333b !important;
|
||||
color: #cdd9e5 !important;
|
||||
border-left: 3px solid #539bf5 !important;
|
||||
}
|
||||
html[data-theme="dark"] code,
|
||||
html[data-theme="dark"] .sig {
|
||||
background-color: #2d333b !important;
|
||||
color: #cdd9e5 !important;
|
||||
}
|
||||
|
||||
/* --- Heading anchors fade-in ------------------------------------------ */
|
||||
a.headerlink { opacity: 0; transition: opacity 120ms; }
|
||||
h1:hover a.headerlink, h2:hover a.headerlink, h3:hover a.headerlink,
|
||||
h4:hover a.headerlink, h5:hover a.headerlink, h6:hover a.headerlink,
|
||||
dt:hover a.headerlink { opacity: 1; }
|
||||
|
||||
/* --- Prev/next strip --------------------------------------------------- */
|
||||
.prev-next-area { overflow: hidden; clear: both; }
|
||||
.prev-next-area .prev-next-title {
|
||||
color: var(--pst-color-link) !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.prev-next-area a:hover .prev-next-title {
|
||||
color: var(--pst-color-link-hover) !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
.prev-next-area .prev-next-subtitle {
|
||||
font-size: 0.78rem !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
color: var(--pst-color-muted) !important;
|
||||
}
|
||||
|
||||
/* --- Back-to-top pill (PyData's built-in #pst-back-to-top) ------------ */
|
||||
#pst-back-to-top {
|
||||
font-family: var(--pst-font-family-base) !important;
|
||||
font-size: 0.82rem !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
text-transform: uppercase !important;
|
||||
padding: 0.65rem 1.6rem !important;
|
||||
border-radius: 2rem !important;
|
||||
border: none !important;
|
||||
background: linear-gradient(135deg, #0066cc, #003a6b) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 4px 14px rgba(0, 60, 120, 0.35) !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
transition: box-shadow 150ms ease, opacity 150ms ease !important;
|
||||
}
|
||||
#pst-back-to-top:hover {
|
||||
box-shadow: 0 6px 20px rgba(0, 60, 120, 0.5) !important;
|
||||
color: #fff !important;
|
||||
text-decoration: none !important;
|
||||
opacity: 0.92 !important;
|
||||
}
|
||||
|
||||
/* --- C++ symbol signatures (Breathe-rendered API pages, when enabled) - */
|
||||
dl.cpp.function > dt.sig,
|
||||
dl.cpp.class > dt.sig,
|
||||
dl.cpp.struct > dt.sig,
|
||||
dl.cpp.type > dt.sig {
|
||||
background: var(--pst-color-surface);
|
||||
border: 1px solid var(--pst-color-border);
|
||||
border-left: 3px solid var(--opencv-accent);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.8;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
dl.cpp.function .sig-name.descname { color: var(--opencv-accent); font-weight: 700; }
|
||||
dl.cpp.function .sig-prename.descclassname { color: var(--pst-color-text-muted); }
|
||||
|
||||
/* --- MathJax inline alignment tweak ----------------------------------- */
|
||||
mjx-container.MathJax:not([display="true"]) { vertical-align: -0.15em !important; }
|
||||
@@ -0,0 +1,20 @@
|
||||
{# Override of pydata_sphinx_theme's components/navbar-nav.html.
|
||||
PyData's default renders both top-level toctree items AND the
|
||||
configured external_links in one bar. For OpenCV we want ONLY the
|
||||
Doxygen-style external nav (Main Page, Related Pages, Namespaces,
|
||||
…) in the header — the tutorial module tree belongs in the left
|
||||
sidebar, not the header. So we render external_links only. #}
|
||||
<nav>
|
||||
<ul class="bd-navbar-elements navbar-nav">
|
||||
{%- for link in (theme_external_links or []) %}
|
||||
<li class="nav-item">
|
||||
{# PyData's CSS adds the ↗ icon via .nav-external::after — don't
|
||||
render a second one inline or it doubles up. #}
|
||||
<a class="nav-link nav-external"
|
||||
href="{{ link.url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{{ link.name }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -0,0 +1,469 @@
|
||||
"""Sphinx wrapper for opencv/doc/.
|
||||
|
||||
The wrapper lives in opencv/docs_sphinx/ as a single conf.py. Sphinx is
|
||||
invoked with config-dir / source-dir separation so the wrapper never
|
||||
duplicates the legacy tree. Build via the CMake `sphinx` target:
|
||||
|
||||
cmake --build <build> --target sphinx
|
||||
# output -> <build>/docs_sphinx/html/
|
||||
|
||||
opencv/doc/ stays untouched: Doxygen-flavored directives in the .markdown
|
||||
sources are translated to MyST in a `source-read` hook below.
|
||||
|
||||
To enable additional tutorial modules, append their directory names (the
|
||||
folder under opencv/doc/tutorials/) to DOC_MODULES below. The root index
|
||||
(tutorials/tutorials.markdown) lists every module, but only modules in
|
||||
DOC_MODULES are actually compiled — entries for the rest are dropped
|
||||
from toctrees automatically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import pathlib, re
|
||||
|
||||
HERE = pathlib.Path(__file__).parent.resolve()
|
||||
DOC_ROOT = (HERE.parent / "doc").resolve()
|
||||
OPENCV_ROOT = HERE.parent.resolve()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SCOPE — add module folder names from opencv/doc/tutorials/ here.
|
||||
# Override via env var to avoid editing this file:
|
||||
# OPENCV_DOC_MODULES=photo,imgproc cmake --build <build> --target sphinx
|
||||
# ---------------------------------------------------------------------------
|
||||
import os as _os
|
||||
DOC_MODULES = [
|
||||
m.strip()
|
||||
for m in (_os.environ.get("OPENCV_DOC_MODULES") or "photo,objdetect").split(",")
|
||||
if m.strip()
|
||||
]
|
||||
|
||||
# -- Project ----------------------------------------------------------------
|
||||
project = "OpenCV"
|
||||
author = "OpenCV Team"
|
||||
release = "5.x"
|
||||
|
||||
# -- Sphinx core ------------------------------------------------------------
|
||||
extensions = ["myst_parser"]
|
||||
for _ext in ("sphinx_design", "sphinx_copybutton"):
|
||||
try:
|
||||
__import__(_ext)
|
||||
extensions.append(_ext)
|
||||
except ImportError:
|
||||
pass
|
||||
HAVE_SPHINX_DESIGN = "sphinx_design" in extensions
|
||||
|
||||
source_suffix = {".md": "markdown", ".markdown": "markdown"}
|
||||
|
||||
# Root tutorial index (lists all modules via @subpage). Stays the master
|
||||
# regardless of how many modules are in DOC_MODULES.
|
||||
master_doc = "tutorials/tutorials"
|
||||
|
||||
# Source dir is opencv/doc/ — scope to the master + enabled modules only.
|
||||
include_patterns = ["tutorials/tutorials.markdown"] + [
|
||||
f"tutorials/{m}/**" for m in DOC_MODULES
|
||||
]
|
||||
exclude_patterns = ["**/Thumbs.db", "**/.DS_Store"]
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence", "deflist", "dollarmath", "amsmath",
|
||||
"attrs_inline", "attrs_block", "smartquotes",
|
||||
]
|
||||
myst_heading_anchors = 4
|
||||
suppress_warnings = ["myst.header", "myst.xref_missing", "toc.not_included"]
|
||||
|
||||
# -- Doxygen integration -----------------------------------------------------
|
||||
# External links in the navbar and unbuilt-module sidebar entries point at
|
||||
# the existing Doxygen build. Override the base URL or tagfile via env vars.
|
||||
DOXYGEN_BASE_URL = (
|
||||
_os.environ.get("OPENCV_DOXYGEN_BASE_URL", "https://docs.opencv.org/5.x/")
|
||||
.rstrip("/") + "/")
|
||||
_TAG_FILE = pathlib.Path(_os.environ.get(
|
||||
"OPENCV_DOXYGEN_TAGFILE",
|
||||
str(HERE.parent.parent / "build" / "doc" / "doxygen" / "html" / "opencv.tag"),
|
||||
))
|
||||
|
||||
# anchor -> doxygen URL filename (from opencv.tag if available).
|
||||
_TAG_FILENAMES: dict[str, str] = {}
|
||||
if _TAG_FILE.is_file():
|
||||
try:
|
||||
import xml.etree.ElementTree as _ET
|
||||
for _c in _ET.parse(str(_TAG_FILE)).getroot().iter("compound"):
|
||||
if _c.get("kind") == "page":
|
||||
_n, _f = _c.findtext("name"), _c.findtext("filename")
|
||||
if _n and _f:
|
||||
_TAG_FILENAMES[_n] = _f if _f.endswith(".html") else _f + ".html"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _doxygen_url(page: str) -> str:
|
||||
return DOXYGEN_BASE_URL + _TAG_FILENAMES.get(page, page)
|
||||
|
||||
# -- HTML / PyData theme ----------------------------------------------------
|
||||
try:
|
||||
import pydata_sphinx_theme # noqa: F401
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
except ImportError:
|
||||
html_theme = "alabaster"
|
||||
|
||||
html_title = "OpenCV Tutorials"
|
||||
html_show_sourcelink = False
|
||||
templates_path = ["_templates"]
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = [
|
||||
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700"
|
||||
"&family=JetBrains+Mono:wght@400;500&display=swap",
|
||||
"custom.css",
|
||||
]
|
||||
html_theme_options = {
|
||||
"logo": {"text": f"OpenCV {release}"},
|
||||
# Show all 7 Doxygen-style external links inline (no "More" dropdown).
|
||||
"header_links_before_dropdown": 7,
|
||||
# Doxygen-style top-level nav (the legacy site's MAIN PAGE / RELATED
|
||||
# PAGES / NAMESPACES / CLASSES / FILES / EXAMPLES / JAVA DOCUMENTATION).
|
||||
# All external — they target the existing Doxygen build.
|
||||
"external_links": [
|
||||
{"url": _doxygen_url("index.html"), "name": "Main Page"},
|
||||
{"url": _doxygen_url("pages.html"), "name": "Related Pages"},
|
||||
{"url": _doxygen_url("namespaces.html"), "name": "Namespaces"},
|
||||
{"url": _doxygen_url("annotated.html"), "name": "Classes"},
|
||||
{"url": _doxygen_url("files.html"), "name": "Files"},
|
||||
{"url": _doxygen_url("examples.html"), "name": "Examples"},
|
||||
{"url": DOXYGEN_BASE_URL + "javadoc/", "name": "Java Documentation"},
|
||||
],
|
||||
"show_toc_level": 2,
|
||||
"navigation_with_keys": True,
|
||||
"show_prev_next": True,
|
||||
"show_nav_level": 2,
|
||||
"navigation_depth": 4,
|
||||
"secondary_sidebar_items": ["page-toc"],
|
||||
"back_to_top_button": True,
|
||||
"show_version_warning_banner": False,
|
||||
"icon_links": [{"name": "GitHub",
|
||||
"url": "https://github.com/opencv/opencv",
|
||||
"icon": "fa-brands fa-github"}],
|
||||
}
|
||||
|
||||
# ===========================================================================
|
||||
# Doxygen-flavored .markdown -> MyST translation via source-read.
|
||||
# Nothing on disk under opencv/doc/ is modified.
|
||||
# ===========================================================================
|
||||
|
||||
# Build anchor maps. Two kinds:
|
||||
# _ANCHOR_TO_DOC anchor -> docname (internal — for enabled modules)
|
||||
# _ANCHOR_TO_EXTERNAL anchor -> (title, url) (external — for the rest)
|
||||
# Disabled modules still appear in the master toctree as external links to
|
||||
# the Doxygen build, so the left sidebar shows the full module list.
|
||||
_ANCHOR_TO_DOC: dict[str, str] = {}
|
||||
_ANCHOR_TO_EXTERNAL: dict[str, tuple[str, str]] = {}
|
||||
|
||||
_HEAD_RE = re.compile(
|
||||
r"^(?P<title1>[^\n]+?)\s*\{#(?P<anchor1>[\w-]+)\}\s*\n[=\-]{3,}\s*$"
|
||||
r"|"
|
||||
r"^#+\s+(?P<title2>[^\n]+?)\s*\{#(?P<anchor2>[\w-]+)\}\s*$",
|
||||
re.MULTILINE)
|
||||
|
||||
def _scan_internal(path: pathlib.Path) -> None:
|
||||
"""Add every {#anchor} in `path` (file or dir) to _ANCHOR_TO_DOC."""
|
||||
files = [path] if (path.is_file() and path.suffix == ".markdown") \
|
||||
else (list(path.rglob("*.markdown")) if path.is_dir() else [])
|
||||
for md in files:
|
||||
try:
|
||||
head = md.read_text(encoding="utf-8", errors="replace")[:4000]
|
||||
except OSError:
|
||||
continue
|
||||
rel = md.relative_to(DOC_ROOT).with_suffix("").as_posix()
|
||||
for m in re.finditer(r"\{#([\w-]+)\}", head):
|
||||
_ANCHOR_TO_DOC[m.group(1)] = rel
|
||||
|
||||
def _scan_external(toc_file: pathlib.Path) -> None:
|
||||
"""Pull the top heading's (title, anchor) from a module's table_of_content
|
||||
file and add it to _ANCHOR_TO_EXTERNAL with a URL into the Doxygen build."""
|
||||
try:
|
||||
head = toc_file.read_text(encoding="utf-8", errors="replace")[:4000]
|
||||
except OSError:
|
||||
return
|
||||
m = _HEAD_RE.search(head)
|
||||
if not m:
|
||||
return
|
||||
anchor = m.group("anchor1") or m.group("anchor2")
|
||||
title = (m.group("title1") or m.group("title2") or "").strip()
|
||||
if not anchor:
|
||||
return
|
||||
url = DOXYGEN_BASE_URL + _TAG_FILENAMES.get(anchor, "index.html")
|
||||
_ANCHOR_TO_EXTERNAL[anchor] = (title, url)
|
||||
|
||||
# Internal scan: master + every enabled module subtree.
|
||||
_scan_internal(DOC_ROOT / "tutorials" / "tutorials.markdown")
|
||||
for _m in DOC_MODULES:
|
||||
_scan_internal(DOC_ROOT / "tutorials" / _m)
|
||||
|
||||
# External scan: every OTHER module's top-level table_of_content_*.markdown.
|
||||
for _toc in (DOC_ROOT / "tutorials").glob("*/table_of_content_*.markdown"):
|
||||
if _toc.parent.name not in DOC_MODULES:
|
||||
_scan_external(_toc)
|
||||
|
||||
# Doxygen flattens IMAGE_PATH across every `images/` folder under the tutorial
|
||||
# tree, so a tutorial can reference `images/foo.png` even when `foo.png` lives
|
||||
# in a sibling module's `images/` directory. Mirror that behavior by building
|
||||
# a basename -> doc-root-relative-path index once at import time.
|
||||
_IMAGE_INDEX: dict[str, str] = {}
|
||||
for _img in (DOC_ROOT / "tutorials").rglob("images/*"):
|
||||
if _img.is_file():
|
||||
_IMAGE_INDEX.setdefault(_img.name, _img.relative_to(DOC_ROOT).as_posix())
|
||||
|
||||
_TOGGLE_LABELS = {"cpp": "C++", "java": "Java", "python": "Python"}
|
||||
|
||||
|
||||
# Mirror of Doxygen's EXAMPLE_PATH (see opencv/doc/Doxyfile.in) — the bases a
|
||||
# bare `@snippet some/path.cpp` is resolved against. OPENCV_ROOT comes first so
|
||||
# fully-qualified paths like `samples/cpp/...` keep working.
|
||||
_SNIPPET_BASES = [
|
||||
OPENCV_ROOT,
|
||||
OPENCV_ROOT / "samples",
|
||||
OPENCV_ROOT / "apps",
|
||||
]
|
||||
|
||||
|
||||
def _read_snippet(rel_path: str, label: str | None) -> tuple[str, str]:
|
||||
"""Return (code_text, language) for an @include / @snippet directive."""
|
||||
p = next((b / rel_path for b in _SNIPPET_BASES
|
||||
if (b / rel_path).is_file()), None)
|
||||
if p is None:
|
||||
return f"// not found: {rel_path}\n", "text"
|
||||
text = p.read_text(encoding="utf-8", errors="replace")
|
||||
ext = p.suffix.lower()
|
||||
lang = {".cpp": "cpp", ".hpp": "cpp", ".h": "cpp", ".c": "c",
|
||||
".py": "python", ".java": "java"}.get(ext, "text")
|
||||
if label is None:
|
||||
return text, lang
|
||||
# Doxygen matches `[label]` after any comment-style marker (//, //!, #, ##)
|
||||
# anywhere on a line — including labels wrapped in block-comments like
|
||||
# `/* //! [label]` or `//! [label] */`.
|
||||
pat = re.compile(r"^[^\[\n]*(?://!|//|##|#)[^\[\n]*\[" + re.escape(label)
|
||||
+ r"\][^\n]*$", re.MULTILINE)
|
||||
matches = list(pat.finditer(text))
|
||||
if len(matches) < 2:
|
||||
return f"// snippet not found: {rel_path} [{label}]\n", lang
|
||||
body = text[matches[0].end():matches[1].start()].strip("\n")
|
||||
lines = body.split("\n")
|
||||
indents = [len(l) - len(l.lstrip(" ")) for l in lines if l.strip()]
|
||||
if indents:
|
||||
dedent = min(indents)
|
||||
lines = [l[dedent:] if len(l) >= dedent else l for l in lines]
|
||||
return "\n".join(lines), lang
|
||||
|
||||
|
||||
def _emit_toggles(tabs: list[tuple[str, str]]) -> str:
|
||||
if HAVE_SPHINX_DESIGN:
|
||||
out = ["", "``````{tab-set}"]
|
||||
for lang, body in tabs:
|
||||
label = _TOGGLE_LABELS.get(lang, lang.title())
|
||||
out += [f"`````{{tab-item}} {label}", body, "`````"]
|
||||
out += ["``````", ""]
|
||||
return "\n".join(out)
|
||||
# Fallback: render each toggle as a labeled section.
|
||||
out = [""]
|
||||
for lang, body in tabs:
|
||||
label = _TOGGLE_LABELS.get(lang, lang.title())
|
||||
out += [f"**{label}**", "", body, ""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _translate(text: str, docname: str | None = None) -> str:
|
||||
# 1. Heading anchors: "Title {#name}\n===" (setext) and "## Title {#name}" (ATX).
|
||||
# Strip the anchor from the rendered heading and emit a MyST label
|
||||
# "(name)=" immediately above. Setext converted to ATX for simplicity.
|
||||
def _setext_repl(m: re.Match) -> str:
|
||||
title = m.group("title").strip()
|
||||
level = 1 if m.group("bar") == "=" else 2
|
||||
return f"({m.group('anchor')})=\n{'#' * level} {title}"
|
||||
text = re.sub(
|
||||
r"^(?P<title>[^\n]+?)\s*\{#(?P<anchor>[\w-]+)\}\s*\n(?P<bar>[=\-])[=\-]{2,}\s*$",
|
||||
_setext_repl, text, flags=re.MULTILINE)
|
||||
text = re.sub(
|
||||
r"^(?P<hashes>#+)\s+(?P<title>[^\n]+?)\s*\{#(?P<anchor>[\w-]+)\}\s*$",
|
||||
lambda m: f"({m.group('anchor')})=\n{m.group('hashes')} {m.group('title')}",
|
||||
text, flags=re.MULTILINE)
|
||||
|
||||
# 2. Doxygen LaTeX math markers
|
||||
text = re.sub(r"\\f\[(.+?)\\f\]",
|
||||
lambda m: f"\n$$\n{m.group(1).strip()}\n$$\n",
|
||||
text, flags=re.DOTALL)
|
||||
text = re.sub(r"\\f\$(.+?)\\f\$", lambda m: f"${m.group(1)}$",
|
||||
text, flags=re.DOTALL)
|
||||
|
||||
# 3. @code{.lang} ... @endcode
|
||||
def _code_repl(m: re.Match) -> str:
|
||||
lang = (m.group("lang") or "").strip(".") or "text"
|
||||
if lang == "none":
|
||||
lang = "text"
|
||||
return f"\n```{lang}\n{m.group('body').strip()}\n```\n"
|
||||
text = re.sub(
|
||||
r"@code(?:\{(?P<lang>[^}]*)\})?\s*\n(?P<body>.*?)\n\s*@endcode",
|
||||
_code_repl, text, flags=re.DOTALL)
|
||||
|
||||
# 4. @include path / @includelineno path
|
||||
# (line numbering hint is dropped — MyST fenced blocks don't take :linenos:
|
||||
# and PyData's code-block styling is already legible without it.)
|
||||
def _include_repl(m: re.Match) -> str:
|
||||
code, lang = _read_snippet(m.group("path"), None)
|
||||
return f"\n```{lang}\n{code.rstrip()}\n```\n"
|
||||
text = re.sub(r"@include(?:lineno)?\s+(?P<path>\S+)", _include_repl, text)
|
||||
|
||||
# 5. @snippet path [Label]
|
||||
def _snippet_repl(m: re.Match) -> str:
|
||||
code, lang = _read_snippet(m.group("path"), m.group("label"))
|
||||
return f"\n```{lang}\n{code.rstrip()}\n```\n"
|
||||
text = re.sub(r"@snippet\s+(?P<path>\S+)\s+(?P<label>[^\n]+?)\s*$",
|
||||
_snippet_repl, text, flags=re.MULTILINE)
|
||||
|
||||
# 6. @add_toggle_LANG ... @end_toggle (coalesce runs into one tab-set)
|
||||
def _toggle_collapse(src: str) -> str:
|
||||
out, i = [], 0
|
||||
opener = re.compile(r"^\s*@add_toggle_(\w+)\s*$", re.MULTILINE)
|
||||
while True:
|
||||
m = opener.search(src, i)
|
||||
if not m:
|
||||
out.append(src[i:]); break
|
||||
out.append(src[i:m.start()])
|
||||
tabs, j = [], m.start()
|
||||
while True:
|
||||
m2 = re.match(
|
||||
r"\s*@add_toggle_(\w+)\s*\n(.*?)\n\s*@end_toggle\s*\n?",
|
||||
src[j:], flags=re.DOTALL)
|
||||
if not m2:
|
||||
break
|
||||
tabs.append((m2.group(1), m2.group(2)))
|
||||
j += m2.end()
|
||||
k = re.match(r"\s*", src[j:])
|
||||
if not k or not re.match(r"@add_toggle_", src[j + k.end():]):
|
||||
break
|
||||
j += k.end()
|
||||
if not tabs:
|
||||
out.append(src[m.start():m.start() + 1]); i = m.start() + 1; continue
|
||||
out.append(_emit_toggles(tabs))
|
||||
i = j
|
||||
return "".join(out)
|
||||
text = _toggle_collapse(text)
|
||||
|
||||
# 7. @ref name [optional "Display Text"]
|
||||
def _ref_repl(m: re.Match) -> str:
|
||||
name = m.group("name"); disp = m.group("disp")
|
||||
target = _ANCHOR_TO_DOC.get(name)
|
||||
if target:
|
||||
return f"[{disp or name}]({'/' + target})"
|
||||
return f"[{disp or name}](#{name})"
|
||||
text = re.sub(r'@ref\s+(?P<name>[\w-]+)(?:\s+"(?P<disp>[^"]+)")?',
|
||||
_ref_repl, text)
|
||||
|
||||
# 8. @cite KEY -> [KEY]
|
||||
text = re.sub(r"@cite\s+([\w-]+)", r"[\1]", text)
|
||||
|
||||
# 8b. @youtube{ID} -> responsive embed (raw HTML, passed through by MyST).
|
||||
text = re.sub(
|
||||
r"^@youtube\{(?P<id>[\w-]+)\}\s*$",
|
||||
lambda m: (
|
||||
'\n<div class="opencv-youtube">'
|
||||
f'<iframe src="https://www.youtube-nocookie.com/embed/{m.group("id")}" '
|
||||
'title="YouTube video player" frameborder="0" '
|
||||
'allow="accelerometer; autoplay; clipboard-write; encrypted-media; '
|
||||
'gyroscope; picture-in-picture" allowfullscreen></iframe></div>\n'
|
||||
),
|
||||
text, flags=re.MULTILINE)
|
||||
|
||||
# 8c. @note ... / @see ... -> MyST admonitions. Each directive body runs
|
||||
# until the next blank line, the next @directive at start-of-line, or
|
||||
# end of file (matches Doxygen's paragraph-level semantics).
|
||||
_ADMON_KIND = {"note": "note", "see": "seealso"}
|
||||
def _admon_repl(m: re.Match) -> str:
|
||||
kind = _ADMON_KIND[m.group("dir")]
|
||||
body = m.group("body").strip()
|
||||
return f"\n:::{{{kind}}}\n{body}\n:::\n"
|
||||
text = re.sub(
|
||||
r"^@(?P<dir>note|see)\s+(?P<body>.+?)(?=\n[ \t]*\n|\n@[A-Za-z]|\Z)",
|
||||
_admon_repl, text, flags=re.DOTALL | re.MULTILINE)
|
||||
|
||||
# 9. @subpage NAME (collected blocks -> real toctree).
|
||||
# Enabled modules' anchors become internal toctree entries.
|
||||
# Disabled modules' anchors become external links into the Doxygen
|
||||
# build, so the left sidebar still shows the full module list.
|
||||
def _subpage_list_to_toctree(src: str) -> str:
|
||||
pat = re.compile(
|
||||
r"((?:^[ \t]*-\s+@subpage\s+[\w-]+(?:[^\n]*)\n)+)", re.MULTILINE)
|
||||
def repl(m: re.Match) -> str:
|
||||
entries = re.findall(r"@subpage\s+([\w-]+)", m.group(1))
|
||||
lines = []
|
||||
for e in entries:
|
||||
if e in _ANCHOR_TO_DOC:
|
||||
lines.append("/" + _ANCHOR_TO_DOC[e])
|
||||
elif e in _ANCHOR_TO_EXTERNAL:
|
||||
title, url = _ANCHOR_TO_EXTERNAL[e]
|
||||
lines.append(f"{title} <{url}>")
|
||||
if not lines:
|
||||
return ""
|
||||
body = "\n".join(lines)
|
||||
return f"\n```{{toctree}}\n:maxdepth: 1\n\n{body}\n```\n"
|
||||
return pat.sub(repl, src)
|
||||
text = _subpage_list_to_toctree(text)
|
||||
|
||||
# 10. @next_tutorial / @prev_tutorial -> drop
|
||||
text = re.sub(r"^@(?:next|prev)_tutorial\{[^}]*\}\s*$", "",
|
||||
text, flags=re.MULTILINE)
|
||||
|
||||
# 11. @tableofcontents -> drop (PyData right sidebar replaces it)
|
||||
text = re.sub(r"^@tableofcontents\s*$", "", text, flags=re.MULTILINE)
|
||||
|
||||
# 11b. @cond NAME ... @endcond -> strip just the markers; if the
|
||||
# enclosed @subpage points to a disabled module it gets dropped
|
||||
# by _subpage_list_to_toctree above.
|
||||
text = re.sub(r"^@cond\s+\S+\s*$", "", text, flags=re.MULTILINE)
|
||||
text = re.sub(r"^@endcond\s*$", "", text, flags=re.MULTILINE)
|
||||
|
||||
# 12. Image paths "images/foo.png" — resolve like Doxygen's flat IMAGE_PATH:
|
||||
# prefer the doc's own "images/" sibling, then fall back to a global
|
||||
# basename lookup across every tutorial `images/` folder. As a final
|
||||
# fallback, point at the consolidated `tutorials/others/images/` dir
|
||||
# (where modules like `photo` store their assets).
|
||||
def _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 m.group(0)
|
||||
hit = _IMAGE_INDEX.get(pathlib.Path(rel).name)
|
||||
if hit:
|
||||
return f'{m.group("pre")}/{hit})'
|
||||
return f'{m.group("pre")}/tutorials/others/images/{rel})'
|
||||
text = re.sub(
|
||||
r'(?P<pre>!\[[^\]]*\]\()images/(?P<rel>[^)]+)\)',
|
||||
_img_repl, text)
|
||||
|
||||
# 13. Front-matter table: OpenCV tutorials use the "| -: | :- |"
|
||||
# alignment pattern for the Original-author/Compatibility block.
|
||||
# Wrap it in a {div} carrying .opencv-meta-table so custom.css
|
||||
# can pin the rounded card + label-column styling without us
|
||||
# modifying the .markdown source.
|
||||
def _wrap_front_matter(src: str) -> str:
|
||||
pat = re.compile(
|
||||
r"(^\|[^\n]*\|[ \t]*\n" # header row (often empty)
|
||||
r"\|[ \t]*-:[ \t]*\|[ \t]*:-[ \t]*\|[ \t]*\n" # alignment row
|
||||
r"(?:\|[^\n]*\|[ \t]*\n)+)", # one or more body rows
|
||||
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)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def _source_read(app, docname, source):
|
||||
# Translate any tutorial doc — the root index plus everything under a
|
||||
# module we enabled in DOC_MODULES.
|
||||
if not docname.startswith("tutorials/"):
|
||||
return
|
||||
source[0] = _translate(source[0], docname)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("source-read", _source_read)
|
||||
return {"parallel_read_safe": True, "parallel_write_safe": True}
|
||||
Reference in New Issue
Block a user