Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bazel/toolchains/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ py_binary(
srcs = ["@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py"],
data = [
"@score_tooling//tools/sphinx:plantuml",
# Expose the project-level custom CSS to all sub-doc Sphinx builds
# (e.g. dependable_element docs) which run in separate Bazel sandboxes
# and cannot access docs/sphinx/_static directly.
"//docs/sphinx:custom_css",
],
exec_compatible_with = ["@platforms//os:linux"],
main = "@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py",
Expand Down
71 changes: 71 additions & 0 deletions bazel/toolchains/template/conf.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,14 @@
'show_prev_next': True,

# Logo configuration
# Sub-doc builds (e.g. dependable_element) are embedded one level deep
# inside the main docs output. Add 'link': '../index.html' so the logo
# navigates back to the main docs. The main docs build has a 'docs/'
# directory sibling in the Bazel sandbox; sub-doc builds do not — use that
# to distinguish the two cases.
'logo': {
'text': 'Eclipse S-CORE',
**({} if (Path(__file__).parent / "docs").is_dir() else {'link': '../index.html'}),
},

# External links - S-CORE GitHub
Expand All @@ -111,12 +117,32 @@
'icon': 'fab fa-github',
}
],

}


# Enable numref for cross-references
numfig = True

# Static assets and custom CSS
# html_static_path is relative to confdir (where this conf.py lives).
# In a local build confdir == the docs source dir, so '_static' is a direct
# sibling. In the Bazel sandbox the generated conf.py sits one level above the
# actual source tree (sphinx_doc/conf.py vs sphinx_doc/docs/sphinx/_static),
# so we search for the _static directory instead of hard-coding the path.
_conf_dir = Path(__file__).parent
_static_local = _conf_dir / "_static"
if _static_local.exists():
html_static_path = ["_static"]
else:
_static_found = next(
(p for p in _conf_dir.rglob("_static") if p.is_dir()), None
)
html_static_path = [str(_static_found)] if _static_found else []

# html_css_files is populated after the runfiles section below once we know
# whether default_custom.css is reachable (local _static or runfiles).

# Load external needs and log configuration
needs_external_needs = bazel_sphinx_needs.load_external_needs()
bazel_sphinx_needs.log_config_info(project)
Expand Down Expand Up @@ -149,6 +175,51 @@
plantuml = f"{_plantuml_path} -Playout=smetana"
plantuml_output_format = "svg_obj"

# Resolve default_custom.css so that sub-doc builds (e.g. dependable_element
# docs) which run in a separate Bazel sandbox also get the project CSS theme.
# The file is declared as data on the sphinx_build binary so it is always
# present in the runfiles tree.
_custom_css_src = None
_css_rloc = r.Rlocation(
"_main/docs/sphinx/_static/css/default_custom.css", source_repo=""
)
if _css_rloc and Path(_css_rloc).exists():
_custom_css_src = Path(_css_rloc)
logger.info(f"Custom CSS resolved from runfiles: {_custom_css_src}")
else:
# Fallback: already present in the locally discovered _static tree
for _sp in html_static_path:
_candidate_css = Path(_sp) / "css" / "default_custom.css"
if _candidate_css.exists():
_custom_css_src = _candidate_css
break

# Only generate the <link> tag when we are certain the file will be available.
html_css_files = ["css/default_custom.css"] if _custom_css_src else []


def _inject_custom_css(app, exception):
"""Write default_custom.css directly into the Sphinx output _static/css/
directory after the build finishes.

Using build-finished (rather than builder-inited + tempfile) avoids any
sandbox write-permission issues in CI: app.outdir is the declared Bazel
output tree, which is always writable during an action. Sphinx has already
run copy_static_files() before this event fires, so our file is not
overwritten, but the <link> tag generated via html_css_files is already
present in every page."""
if exception is not None or _custom_css_src is None:
return
import shutil

css_dir = Path(app.outdir) / "_static" / "css"
css_dir.mkdir(parents=True, exist_ok=True)
dest = css_dir / "default_custom.css"
if not dest.exists():
shutil.copy(str(_custom_css_src), str(dest))
logger.info(f"Custom CSS written to output: {dest}")


def setup(app):
app.connect("build-finished", _inject_custom_css)
return bazel_sphinx_needs.setup_sphinx_extension(app, needs_external_needs)
19 changes: 16 additions & 3 deletions docs/sphinx/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,28 @@ sphinx_docs_library(
srcs = ["//score/mw/com/design/doxygen_build:generate_doxygen"],
)

# Expose version flyout assets so docs/sphinx/utils targets can reference them
exports_files(
[
"_static/css/version_flyout.css",
"_static/js/version_flyout.js",
],
visibility = ["//docs/sphinx/utils:__pkg__"],
)

# Static assets for Sphinx documentation
filegroup(
name = "static_assets",
srcs = glob(["_static/**/*"]),
)

exports_files(
glob(["_static/**/*"]),
visibility = ["//docs/sphinx/utils:__pkg__"],
# Custom CSS exposed so it can be added to the sphinx_build runfiles and
# therefore be available to sub-doc builds (e.g. dependable_element docs)
# that are built in a separate Bazel sandbox.
filegroup(
name = "custom_css",
srcs = ["_static/css/default_custom.css"],
visibility = ["//bazel/toolchains:__pkg__"],
)

# Generate RST files from @api tagged items using custom Starlark rule
Expand Down
148 changes: 43 additions & 105 deletions docs/sphinx/_static/css/default_custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,16 @@ html[data-theme="dark"] {
/* ============================================================================
* Navigation Styles
* ============================================================================ */
/* Search box in navbar/header - always light background with dark text */
/* Search box in navbar/header */
.bd-header input.search-input,
.bd-header .search-button-field,
.bd-header input[type="search"],
.navbar input.search-input,
.navbar .search-button-field,
.navbar input[type="search"] {
color: #333333 ;
background-color: #ffffff ;
border: none ;
color: #FFFFFF !important;
background-color: transparent !important;
border: none !important;
}

.bd-header input.search-input::placeholder,
Expand Down Expand Up @@ -141,16 +141,6 @@ html .pst-navbar-icon:hover {
border-bottom: 1px solid var(--pst-color-primary);
}

/* Version Switcher Button */
button.btn.version-switcher__button {
color: #FFF;
}

/* Version Switcher Menu */
.version-switcher__menu a.list-group-item {
color: #FFF;
}

/* Primary navigation styles */
.navbar-brand {
color: #FFFFFF ;
Expand Down Expand Up @@ -186,97 +176,45 @@ button.btn.version-switcher__button {
color: #FFFFFF ;
}

/* Override for search input - must come after the wildcard rule above */
/* Light theme search box - light background with dark/purple text */
html[data-theme="light"] .bd-header input,
html[data-theme="light"] .bd-header input.search-input,
html[data-theme="light"] .bd-header input[type="search"],
html[data-theme="light"] .bd-header .search-button-field input,
html[data-theme="light"] .bd-header .search-button__button,
html[data-theme="light"] .bd-header button.search-button__button,
html[data-theme="light"] .bd-header .search-button,
html[data-theme="light"] .navbar input,
html[data-theme="light"] .navbar input.search-input,
html[data-theme="light"] .navbar input[type="search"] {
color: #7c4daa;
background-color: #ffffff;
transition: background-color 0.2s ease, color 0.2s ease;
}

/* Hover effect for light theme search box */
html[data-theme="light"] .bd-header .search-button__button:hover,
html[data-theme="light"] .bd-header button.search-button__button:hover {
background-color: #e8d4f5 !important;
color: #7c4daa !important;
}

/* Light theme - search button text and icon - force same color as text */
html[data-theme="light"] .bd-header .search-button__default-text,
html[data-theme="light"] .bd-header .search-button__kbd-shortcut,
html[data-theme="light"] .navbar .search-button__default-text,
html[data-theme="light"] .navbar .search-button__kbd-shortcut {
color: #7c4daa;
}

/* Light theme - SVG icon - must match text color */
html[data-theme="light"] .bd-header .search-button svg,
html[data-theme="light"] .bd-header .search-button svg *,
html[data-theme="light"] .bd-header .search-button svg path,
html[data-theme="light"] .bd-header .search-button svg circle,
html[data-theme="light"] .navbar .search-button svg,
html[data-theme="light"] .navbar .search-button svg *,
html[data-theme="light"] .navbar .search-button svg path,
html[data-theme="light"] .navbar .search-button svg circle {
color: #7c4daa;
fill: none;
stroke: #7c4daa;
stroke-width: 2;
}

/* Dark theme search box - gray background with white text */
html[data-theme="dark"] .bd-header input,
html[data-theme="dark"] .bd-header input.search-input,
html[data-theme="dark"] .bd-header input[type="search"],
html[data-theme="dark"] .bd-header .search-button-field input,
html[data-theme="dark"] .bd-header .search-button__button,
html[data-theme="dark"] .bd-header button.search-button__button,
html[data-theme="dark"] .bd-header .search-button,
html[data-theme="dark"] .navbar input,
html[data-theme="dark"] .navbar input.search-input,
html[data-theme="dark"] .navbar input[type="search"] {
color: #ffffff;
background-color: #4a4a4a;
transition: background-color 0.2s ease, color 0.2s ease;
}

/* Hover effect for dark theme search box */
html[data-theme="dark"] .bd-header .search-button__button:hover,
html[data-theme="dark"] .bd-header button.search-button__button:hover {
background-color: #d4c0e8 !important;
color: #2D1942 !important;
}

/* Dark theme - search button text and icon */
html[data-theme="dark"] .bd-header .search-button__default-text,
html[data-theme="dark"] .bd-header .search-button__kbd-shortcut,
html[data-theme="dark"] .navbar .search-button__default-text,
html[data-theme="dark"] .navbar .search-button__kbd-shortcut {
color: #ffffff;
}

/* Dark theme - SVG icon - must match text color */
html[data-theme="dark"] .bd-header .search-button svg,
html[data-theme="dark"] .bd-header .search-button svg *,
html[data-theme="dark"] .bd-header .search-button svg path,
html[data-theme="dark"] .bd-header .search-button svg circle,
html[data-theme="dark"] .navbar .search-button svg,
html[data-theme="dark"] .navbar .search-button svg *,
html[data-theme="dark"] .navbar .search-button svg path,
html[data-theme="dark"] .navbar .search-button svg circle {
color: #ffffff;
fill: none;
stroke: #ffffff;
stroke-width: 2;
/* Header search button styling */
.bd-header .search-button__button,
.bd-header button.search-button__button,
.bd-header .search-button {
color: #e6c8ff !important;
background-color: rgba(214, 171, 255, 0.18) !important;
border: 2px solid #b784e4 !important;
border-radius: 999px !important;
box-shadow: none !important;
transition: background-color 0.2s ease, border-color 0.2s ease,
color 0.2s ease !important;
}

.bd-header .search-button__button:hover,
.bd-header button.search-button__button:hover {
background-color: rgba(214, 171, 255, 0.24) !important;
border-color: #c89af0 !important;
color: #f1dbff !important;
}

.bd-header .search-button__default-text,
.bd-header .search-button__kbd-shortcut,
.navbar .search-button__default-text,
.navbar .search-button__kbd-shortcut {
color: #e6c8ff !important;
}

.bd-header .search-button svg,
.bd-header .search-button svg *,
.bd-header .search-button svg path,
.bd-header .search-button svg circle,
.navbar .search-button svg,
.navbar .search-button svg *,
.navbar .search-button svg path,
.navbar .search-button svg circle {
color: #f3e5ff !important;
fill: none !important;
stroke: #f3e5ff !important;
stroke-width: 2 !important;
}

.bd-header a {
Expand Down
3 changes: 3 additions & 0 deletions docs/sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,16 @@
'icon': 'fab fa-github',
}
],

}

# Add custom styling
html_static_path = ['_static']
html_css_files = [
'css/default_custom.css',
]
# Note: version_flyout.css and version_flyout.js are injected by the
# deploy workflow via _shared/ paths so they load once across all versions.

# -- Breathe configuration --
# Doxygen XML output path (provided by sphinx_docs_library)
Expand Down
19 changes: 16 additions & 3 deletions docs/sphinx/utils/assemble_publish_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

import argparse
import json
import os
import pathlib
import shutil
import sys
Expand All @@ -48,6 +49,18 @@
_CSS = _STATIC / "css" / "version_flyout.css"
_JS = _STATIC / "js" / "version_flyout.js"

# When run via `bazel run`, the CWD is not necessarily the workspace root.
# Bazel sets BUILD_WORKSPACE_DIRECTORY to the workspace root so that scripts
# can resolve relative paths passed from the workflow correctly.
_WORKSPACE = pathlib.Path(os.environ.get("BUILD_WORKSPACE_DIRECTORY", os.getcwd()))


def _resolve(p: str) -> pathlib.Path:
"""Resolve a path relative to the Bazel workspace root if not absolute."""
path = pathlib.Path(p)
return path if path.is_absolute() else _WORKSPACE / path


def main() -> int:
parser = argparse.ArgumentParser(
description="Assemble the GitHub Pages publish tree for versioned Sphinx docs."
Expand All @@ -66,8 +79,8 @@ def main() -> int:
help="Path to the root index.html (redirect page)")
args = parser.parse_args()

publish = pathlib.Path(args.publish_dir)
docs_out = pathlib.Path(args.docs_output)
publish = _resolve(args.publish_dir)
docs_out = _resolve(args.docs_output)
version = args.version
is_tag = args.is_tag == "true"
repo_url = args.repo_url.rstrip("/")
Expand Down Expand Up @@ -107,7 +120,7 @@ def main() -> int:
shutil.copy2(_JS, shared_js_dir / _JS.name)

# ── Root files ────────────────────────────────────────────────────────────
shutil.copy2(args.root_index, publish / "index.html")
shutil.copy2(_resolve(args.root_index), publish / "index.html")
(publish / ".nojekyll").touch()

# ── switcher.json ─────────────────────────────────────────────────────────
Expand Down
Loading