mkdocs-terok 0.5.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: 0BSD
3
+
4
+ """Shared ProperDocs documentation generators for terok projects.
5
+
6
+ Provides reusable modules for CI maps, test maps, quality reports,
7
+ API reference pages, and Pydantic config reference rendering.
8
+ The ``terok`` ProperDocs plugin wraps all generators; individual modules
9
+ remain usable standalone (they never import properdocs themselves).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ __version__ = "0.5.6" # managed by poetry-dynamic-versioning
17
+
18
+
19
+ def brand_css_path() -> Path:
20
+ """Return the filesystem path to the shared brand CSS file."""
21
+ return Path(__file__).parent / "_assets" / "extra.css"
22
+
23
+
24
+ def mermaid_zoom_js_path() -> Path:
25
+ """Return the filesystem path to the Mermaid diagram zoom script."""
26
+ return Path(__file__).parent / "_assets" / "mermaid_zoom.js"
@@ -0,0 +1,211 @@
1
+ /* Black/Red/Gold palette with subdued neutrals. */
2
+ :root {
3
+ --terok-black: #0b0b0b;
4
+ --terok-red: #a31219;
5
+ --terok-red-dark: #7f0f14;
6
+ --terok-red-pure: #c00000;
7
+ --terok-red-bright: #e04b3f;
8
+ --terok-gold: #c79a22;
9
+ --terok-gold-bright: #d8b454;
10
+ --terok-gold-link: #7a6200;
11
+ --terok-red-hover: #8b0000;
12
+
13
+ --md-text-font: "Ubuntu", sans-serif;
14
+ --md-code-font: "Ubuntu Mono", monospace;
15
+
16
+ --md-primary-fg-color: var(--terok-red-dark);
17
+ --md-primary-fg-color--light: #8f1117;
18
+ --md-primary-fg-color--dark: #650b10;
19
+
20
+ --md-primary-bg-color: #f7f2e8;
21
+ --md-primary-bg-color--light: #f7f2e8;
22
+ --md-primary-bg-color--dark: #e7decc;
23
+
24
+ --md-accent-fg-color: var(--terok-gold);
25
+ --md-accent-fg-color--transparent: rgba(199, 154, 34, 0.12);
26
+
27
+ --md-typeset-a-color: var(--terok-gold-link);
28
+ --md-typeset-mark-color: var(--terok-gold-bright);
29
+ }
30
+
31
+ body,
32
+ .md-typeset,
33
+ .md-header,
34
+ .md-tabs,
35
+ .md-nav,
36
+ .md-search__input {
37
+ font-family: var(--md-text-font);
38
+ }
39
+
40
+ pre,
41
+ code,
42
+ kbd,
43
+ samp {
44
+ font-family: var(--md-code-font);
45
+ }
46
+
47
+ .md-header,
48
+ .md-tabs,
49
+ .md-header__inner,
50
+ .md-tabs__inner {
51
+ background-color: var(--terok-red-dark) !important;
52
+ }
53
+
54
+ .md-typeset a,
55
+ .md-typeset a:visited {
56
+ color: var(--terok-gold-link);
57
+ }
58
+
59
+ .md-typeset a:hover,
60
+ .md-nav__link:hover,
61
+ .md-tabs__link:hover,
62
+ .md-header__button:hover {
63
+ color: var(--terok-red-hover) !important;
64
+ }
65
+
66
+ .md-nav__item--active > .md-nav__link,
67
+ .md-nav__link--active,
68
+ .md-nav--secondary .md-nav__link--active {
69
+ color: var(--terok-gold-bright) !important;
70
+ }
71
+
72
+ .md-nav--secondary .md-nav__link--active::before,
73
+ .md-nav--secondary .md-nav__link--active::after {
74
+ background-color: var(--terok-gold-bright);
75
+ }
76
+
77
+ .md-nav--secondary .md-nav__link--active {
78
+ border-left-color: var(--terok-gold-bright);
79
+ }
80
+
81
+ .md-tabs__link--active {
82
+ color: inherit !important;
83
+ font-weight: 700;
84
+ }
85
+
86
+ #codecov-treemap-img {
87
+ display: block;
88
+ min-width: 300px;
89
+ width: 60%;
90
+ margin: 0 auto;
91
+ }
92
+
93
+ [data-md-color-scheme="default"] {
94
+ --md-primary-fg-color: var(--terok-red-dark);
95
+ --md-primary-fg-color--light: #8f1117;
96
+ --md-primary-fg-color--dark: #650b10;
97
+ --md-default-bg-color: #f7f2e8;
98
+ --md-default-fg-color: #111111;
99
+ --md-default-fg-color--light: #2b2b2b;
100
+ --md-default-fg-color--lighter: #4a4a4a;
101
+ --md-default-fg-color--lightest: #6a6a6a;
102
+ --md-code-bg-color: #efe6d6;
103
+ --md-code-fg-color: #111111;
104
+ --md-footer-bg-color: #0b0b0b;
105
+ --md-footer-fg-color: #f7f2e8;
106
+ --md-footer-fg-color--light: #d9cfbd;
107
+ --md-footer-fg-color--lighter: #bdb39f;
108
+ --md-footer-fg-color--lightest: #a79c87;
109
+ --md-accent-fg-color: var(--terok-gold);
110
+ --md-accent-fg-color--transparent: rgba(199, 154, 34, 0.12);
111
+ --md-typeset-a-color: var(--terok-gold-link);
112
+ }
113
+
114
+ [data-md-color-scheme="slate"] {
115
+ --md-primary-fg-color: var(--terok-red-dark);
116
+ --md-primary-fg-color--light: #8f1117;
117
+ --md-primary-fg-color--dark: #650b10;
118
+ --md-default-bg-color: #0b0b0b;
119
+ --md-default-fg-color: #f2ead9;
120
+ --md-default-fg-color--light: #d8cfbf;
121
+ --md-default-fg-color--lighter: #bcb4a5;
122
+ --md-default-fg-color--lightest: #a19a8d;
123
+ --md-code-bg-color: #151515;
124
+ --md-code-fg-color: #f2ead9;
125
+ --md-footer-bg-color: #000000;
126
+ --md-footer-fg-color: #f2ead9;
127
+ --md-footer-fg-color--light: #d8cfbf;
128
+ --md-footer-fg-color--lighter: #bcb4a5;
129
+ --md-footer-fg-color--lightest: #a19a8d;
130
+
131
+ --md-primary-bg-color: #f2ead9;
132
+ --md-primary-bg-color--light: #f2ead9;
133
+ --md-primary-bg-color--dark: #d8cfbf;
134
+
135
+ --md-accent-fg-color: var(--terok-gold-bright);
136
+ --md-accent-fg-color--transparent: rgba(216, 180, 84, 0.14);
137
+ --md-typeset-a-color: var(--terok-gold-bright);
138
+ }
139
+
140
+ /* ── Mermaid diagram zoom overlay ───────────────────────────── */
141
+ .mermaid-zoom-wrapper {
142
+ position: relative;
143
+ }
144
+
145
+ .mermaid-zoom-btn {
146
+ position: absolute;
147
+ top: 4px;
148
+ right: 4px;
149
+ z-index: 1;
150
+ padding: 2px 8px;
151
+ border: 1px solid var(--md-default-fg-color--lightest);
152
+ border-radius: 4px;
153
+ background: var(--md-default-bg-color);
154
+ color: var(--md-default-fg-color--light);
155
+ font: 0.7rem var(--md-text-font);
156
+ cursor: pointer;
157
+ }
158
+
159
+ .mermaid-zoom-overlay {
160
+ display: none;
161
+ position: fixed;
162
+ inset: 0;
163
+ z-index: 9999;
164
+ background: rgba(0, 0, 0, 0.75);
165
+ backdrop-filter: blur(4px);
166
+ }
167
+
168
+ .mermaid-zoom-overlay.mermaid-zoom-active {
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ }
173
+
174
+ .mermaid-zoom-viewport {
175
+ display: flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ width: calc(100vw - 4rem);
179
+ height: calc(100vh - 4rem);
180
+ padding: 1rem;
181
+ border-radius: 8px;
182
+ background: var(--md-default-bg-color);
183
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
184
+ overflow: auto;
185
+ }
186
+
187
+ .mermaid-zoom-viewport svg {
188
+ max-width: 100%;
189
+ max-height: 100%;
190
+ }
191
+
192
+ .mermaid-zoom-close {
193
+ position: absolute;
194
+ top: 0.75rem;
195
+ right: 0.75rem;
196
+ z-index: 10000;
197
+ width: 2rem;
198
+ height: 2rem;
199
+ border: none;
200
+ border-radius: 50%;
201
+ background: var(--terok-red-dark);
202
+ color: #fff;
203
+ font-size: 1.1rem;
204
+ line-height: 1;
205
+ cursor: pointer;
206
+ transition: background 0.15s;
207
+ }
208
+
209
+ .mermaid-zoom-close:hover {
210
+ background: var(--terok-red);
211
+ }
@@ -0,0 +1,128 @@
1
+ // SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ // SPDX-License-Identifier: 0BSD
3
+
4
+ /**
5
+ * Adds an "Enlarge" button to each rendered Mermaid diagram.
6
+ * Clicking it opens the diagram in a fullscreen overlay (lightbox-style).
7
+ * Close via backdrop click, the X button, or Escape.
8
+ */
9
+ ;(() => {
10
+ const BUTTON_LABEL = "\u2922 Enlarge"
11
+
12
+ // ── Main action ────────────────────────────────────────
13
+
14
+ /** Scan the DOM for rendered mermaid SVGs and attach buttons. */
15
+ function scan() {
16
+ document.querySelectorAll("pre.mermaid, .mermaid").forEach((el) => {
17
+ if (el.querySelector("svg") || el.tagName === "SVG") attachButton(el)
18
+ })
19
+ }
20
+
21
+ /** Wrap a rendered mermaid container and inject the enlarge button. */
22
+ function attachButton(container) {
23
+ if (container.dataset.zoomAttached) return
24
+ container.dataset.zoomAttached = "1"
25
+
26
+ const wrapper = document.createElement("div")
27
+ wrapper.className = "mermaid-zoom-wrapper"
28
+ container.parentNode.insertBefore(wrapper, container)
29
+ wrapper.appendChild(container)
30
+
31
+ const btn = document.createElement("button")
32
+ btn.className = "mermaid-zoom-btn"
33
+ btn.textContent = BUTTON_LABEL
34
+ btn.addEventListener("click", () => {
35
+ openOverlay(container.querySelector("svg") ?? container)
36
+ })
37
+ container.before(btn)
38
+ }
39
+
40
+ // ── Overlay mechanics ──────────────────────────────────
41
+
42
+ function openOverlay(svgSource) {
43
+ const overlay =
44
+ document.querySelector(".mermaid-zoom-overlay") || createOverlay()
45
+ const viewport = overlay.querySelector(".mermaid-zoom-viewport")
46
+ viewport.innerHTML = ""
47
+ const clone = svgSource.cloneNode(true)
48
+ clone.removeAttribute("height")
49
+ clone.removeAttribute("width")
50
+ clone.style.maxWidth = "100%"
51
+ clone.style.maxHeight = "100%"
52
+ clone.style.width = "auto"
53
+ clone.style.height = "auto"
54
+ viewport.appendChild(clone)
55
+ overlay.classList.add("mermaid-zoom-active")
56
+ document.addEventListener("keydown", onEscape)
57
+ }
58
+
59
+ function closeOverlay(overlay) {
60
+ overlay.classList.remove("mermaid-zoom-active")
61
+ document.removeEventListener("keydown", onEscape)
62
+ }
63
+
64
+ function onEscape(e) {
65
+ if (e.key === "Escape") {
66
+ const overlay = document.querySelector(".mermaid-zoom-overlay")
67
+ if (overlay) closeOverlay(overlay)
68
+ }
69
+ }
70
+
71
+ /** Build the overlay element (singleton, appended on first use). */
72
+ function createOverlay() {
73
+ const overlay = document.createElement("div")
74
+ overlay.className = "mermaid-zoom-overlay"
75
+ overlay.addEventListener("click", (e) => {
76
+ if (e.target === overlay) closeOverlay(overlay)
77
+ })
78
+
79
+ const close = document.createElement("button")
80
+ close.className = "mermaid-zoom-close"
81
+ close.textContent = "\u2715"
82
+ close.title = "Close"
83
+ close.addEventListener("click", () => closeOverlay(overlay))
84
+
85
+ const viewport = document.createElement("div")
86
+ viewport.className = "mermaid-zoom-viewport"
87
+
88
+ overlay.append(close, viewport)
89
+ document.body.appendChild(overlay)
90
+ return overlay
91
+ }
92
+
93
+ // ── Initialization ─────────────────────────────────────
94
+
95
+ // Initial scan once DOM + mermaid rendering settle.
96
+ if (document.readyState === "loading") {
97
+ document.addEventListener("DOMContentLoaded", () => setTimeout(scan, 2000))
98
+ } else {
99
+ setTimeout(scan, 2000)
100
+ }
101
+
102
+ // Catch diagrams rendered after initial load (e.g. instant navigation).
103
+ const observer = new MutationObserver((mutations) => {
104
+ for (const m of mutations) {
105
+ for (const node of m.addedNodes) {
106
+ if (node.nodeType !== 1) continue
107
+ if (
108
+ (node.matches?.(".mermaid, pre.mermaid") && node.querySelector("svg")) ||
109
+ node.querySelector?.(".mermaid svg, pre.mermaid svg") ||
110
+ (node.tagName === "svg" && node.parentElement?.matches?.(".mermaid, pre.mermaid"))
111
+ ) {
112
+ setTimeout(scan, 200)
113
+ return
114
+ }
115
+ }
116
+ }
117
+ })
118
+
119
+ function startObserver() {
120
+ observer.observe(document.body, { childList: true, subtree: true })
121
+ }
122
+
123
+ if (document.body) {
124
+ startObserver()
125
+ } else {
126
+ document.addEventListener("DOMContentLoaded", startObserver, { once: true })
127
+ }
128
+ })()
mkdocs_terok/ci_map.py ADDED
@@ -0,0 +1,174 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: 0BSD
3
+
4
+ """Generate a Markdown map of GitHub workflows and jobs.
5
+
6
+ Parses ``.github/workflows/*.yml`` and ``.yaml`` files and produces a
7
+ Markdown document with workflow summary and per-job detail tables.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+ # ── Entry point ─────────────────────────────────────────
18
+
19
+
20
+ def generate_ci_map(
21
+ workflows: list[dict[str, object]] | None = None,
22
+ *,
23
+ workflows_dir: Path | None = None,
24
+ ) -> str:
25
+ """Generate the Markdown CI map.
26
+
27
+ Args:
28
+ workflows: Pre-loaded workflow data. If ``None``, loads from disk.
29
+ workflows_dir: Override for the workflows directory.
30
+ """
31
+ if workflows is None:
32
+ workflows = load_workflows(workflows_dir)
33
+ job_count = sum(len(workflow["jobs"]) for workflow in workflows)
34
+ now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")
35
+
36
+ lines = [
37
+ "# CI Workflow Map\n\n",
38
+ f"*Generated: {now}*\n\n",
39
+ f"**{len(workflows)} workflows** with **{job_count} jobs**\n\n",
40
+ "## Workflows\n\n",
41
+ "| Workflow | File | Triggers | Jobs |\n",
42
+ "|---|---|---|---|\n",
43
+ ]
44
+ for workflow in workflows:
45
+ lines.append(
46
+ f"| `{workflow['name']}` | `{workflow['file_name']}` | "
47
+ f"{workflow['triggers']} | {len(workflow['jobs'])} |\n"
48
+ )
49
+
50
+ lines.extend(
51
+ [
52
+ "\n## Jobs\n\n",
53
+ "| Workflow | Job | Needs | Uploads | Downloads |\n",
54
+ "|---|---|---|---|---|\n",
55
+ ]
56
+ )
57
+ for workflow in workflows:
58
+ for job in workflow["jobs"]:
59
+ lines.append(
60
+ f"| `{workflow['name']}` | `{job['name']}` | {_render(job['needs'])} | "
61
+ f"{_render(job['uploads'])} | {_render(job['downloads'])} |\n"
62
+ )
63
+
64
+ lines.append("\n")
65
+ return "".join(lines)
66
+
67
+
68
+ # ── Workflow loading ────────────────────────────────────
69
+
70
+
71
+ def load_workflows(workflows_dir: Path | None = None) -> list[dict[str, object]]:
72
+ """Load workflow and job facts from ``.github/workflows/*.yml``.
73
+
74
+ Args:
75
+ workflows_dir: Directory containing workflow YAML files.
76
+ Defaults to ``.github/workflows/`` relative to cwd.
77
+ """
78
+ if workflows_dir is None:
79
+ workflows_dir = Path.cwd() / ".github" / "workflows"
80
+ workflows: list[dict[str, object]] = []
81
+ yml_files = list(workflows_dir.glob("*.yml")) + list(workflows_dir.glob("*.yaml"))
82
+ for path in sorted(yml_files):
83
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
84
+ if not isinstance(data, dict):
85
+ continue
86
+
87
+ jobs: list[dict[str, object]] = []
88
+ jobs_section = data.get("jobs", {})
89
+ if isinstance(jobs_section, dict):
90
+ for job_id, job_data in jobs_section.items():
91
+ if not isinstance(job_data, dict):
92
+ continue
93
+ needs = job_data.get("needs", ())
94
+ needs_tuple = (
95
+ (needs,)
96
+ if isinstance(needs, str)
97
+ else tuple(str(item) for item in needs)
98
+ if isinstance(needs, list)
99
+ else ()
100
+ )
101
+ jobs.append(
102
+ {
103
+ "name": str(job_data.get("name", job_id)),
104
+ "needs": needs_tuple,
105
+ "uploads": _artifact_names(
106
+ job_data.get("steps"), "actions/upload-artifact"
107
+ ),
108
+ "downloads": _artifact_names(
109
+ job_data.get("steps"), "actions/download-artifact"
110
+ ),
111
+ }
112
+ )
113
+
114
+ workflows.append(
115
+ {
116
+ "file_name": path.name,
117
+ "name": str(data.get("name", path.stem)),
118
+ "triggers": _trigger_summary(data),
119
+ "jobs": jobs,
120
+ }
121
+ )
122
+ return workflows
123
+
124
+
125
+ # ── Formatting helpers ──────────────────────────────────
126
+
127
+
128
+ def _artifact_names(steps: object, prefix: str) -> tuple[str, ...]:
129
+ """Collect upload/download artifact names from a list of steps."""
130
+ if not isinstance(steps, list):
131
+ return ()
132
+ names: list[str] = []
133
+ for step in steps:
134
+ if not isinstance(step, dict):
135
+ continue
136
+ uses = step.get("uses")
137
+ if not isinstance(uses, str) or not uses.startswith(prefix):
138
+ continue
139
+ with_section = step.get("with", {})
140
+ if isinstance(with_section, dict):
141
+ name = with_section.get("name")
142
+ names.append(str(name) if name else "(all artifacts)")
143
+ return tuple(names)
144
+
145
+
146
+ def _trigger_summary(data: dict[object, object]) -> str:
147
+ """Render the top-level ``on`` section as a compact string."""
148
+ on_section = data.get("on", data.get(True, {}))
149
+ if isinstance(on_section, str):
150
+ return f"`{on_section}`"
151
+ if isinstance(on_section, list):
152
+ return ", ".join(f"`{item}`" for item in on_section)
153
+ if not isinstance(on_section, dict):
154
+ return "—"
155
+
156
+ parts: list[str] = []
157
+ for name, value in on_section.items():
158
+ suffix = ""
159
+ if isinstance(value, dict):
160
+ if name in {"push", "pull_request", "pull_request_target"}:
161
+ branches = value.get("branches")
162
+ if isinstance(branches, list) and branches:
163
+ suffix = f"({', '.join(str(branch) for branch in branches)})"
164
+ elif name == "workflow_run":
165
+ workflows = value.get("workflows")
166
+ if isinstance(workflows, list) and workflows:
167
+ suffix = f"({', '.join(str(workflow) for workflow in workflows)})"
168
+ parts.append(f"`{name}{suffix}`")
169
+ return ", ".join(parts) if parts else "—"
170
+
171
+
172
+ def _render(values: tuple[str, ...]) -> str:
173
+ """Render a tuple of values into one Markdown table cell."""
174
+ return "<br>".join(f"`{value}`" for value in values) if values else "—"