mirror of
https://github.com/opencv/opencv.git
synced 2026-09-19 15:23:23 -05: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
|
# Generate targets for documentation
|
||||||
add_subdirectory(doc)
|
add_subdirectory(doc)
|
||||||
|
add_subdirectory(docs_sphinx)
|
||||||
|
|
||||||
# various data that is used by cv libraries and/or demo applications.
|
# various data that is used by cv libraries and/or demo applications.
|
||||||
# add_subdirectory(data)
|
# add_subdirectory(data)
|
||||||
@@ -1310,6 +1311,7 @@ ocv_build_features_string(docs_status
|
|||||||
IF TARGET doxygen_cpp THEN "doxygen"
|
IF TARGET doxygen_cpp THEN "doxygen"
|
||||||
IF TARGET doxygen_python THEN "python"
|
IF TARGET doxygen_python THEN "python"
|
||||||
IF TARGET doxygen_javadoc THEN "javadoc"
|
IF TARGET doxygen_javadoc THEN "javadoc"
|
||||||
|
IF TARGET sphinx THEN "sphinx"
|
||||||
IF BUILD_opencv_js OR DEFINED OPENCV_JS_LOCATION THEN "js"
|
IF BUILD_opencv_js OR DEFINED OPENCV_JS_LOCATION THEN "js"
|
||||||
ELSE "NO"
|
ELSE "NO"
|
||||||
)
|
)
|
||||||
|
|||||||
40
docs_sphinx/CMakeLists.txt
Normal file
40
docs_sphinx/CMakeLists.txt
Normal file
@@ -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})")
|
||||||
419
docs_sphinx/_static/custom.css
Normal file
419
docs_sphinx/_static/custom.css
Normal file
@@ -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; }
|
||||||
20
docs_sphinx/_templates/navbar-nav.html
Normal file
20
docs_sphinx/_templates/navbar-nav.html
Normal file
@@ -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>
|
||||||
469
docs_sphinx/conf.py
Normal file
469
docs_sphinx/conf.py
Normal file
@@ -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