relcon-docs 0.1.0__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.
- relcon_docs/__init__.py +1 -0
- relcon_docs/assets/base.yml +63 -0
- relcon_docs/assets/docs.yml +23 -0
- relcon_docs/assets/relcon-docs-math.js +34 -0
- relcon_docs/assets/relcon-docs.css +111 -0
- relcon_docs/cli.py +243 -0
- relcon_docs-0.1.0.dist-info/METADATA +262 -0
- relcon_docs-0.1.0.dist-info/RECORD +11 -0
- relcon_docs-0.1.0.dist-info/WHEEL +4 -0
- relcon_docs-0.1.0.dist-info/entry_points.txt +2 -0
- relcon_docs-0.1.0.dist-info/licenses/LICENSE +21 -0
relcon_docs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared documentation configuration for Relaxed Constraints projects."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Managed by relcon-docs configure. Do not edit this file.
|
|
2
|
+
# Put project overrides in mkdocs.yml. Paths are relative to that file.
|
|
3
|
+
extra_css:
|
|
4
|
+
- stylesheets/relcon-docs.css
|
|
5
|
+
extra_javascript:
|
|
6
|
+
- javascripts/relcon-docs-math.js
|
|
7
|
+
theme:
|
|
8
|
+
variant: modern
|
|
9
|
+
font: false
|
|
10
|
+
features:
|
|
11
|
+
- content.action.copy
|
|
12
|
+
- content.tabs.link
|
|
13
|
+
- content.code.copy
|
|
14
|
+
- navigation.tabs
|
|
15
|
+
- navigation.top
|
|
16
|
+
- search.highlight
|
|
17
|
+
palette:
|
|
18
|
+
- media: (prefers-color-scheme)
|
|
19
|
+
toggle:
|
|
20
|
+
icon: lucide/sun-moon
|
|
21
|
+
name: Switch to light mode
|
|
22
|
+
- media: '(prefers-color-scheme: light)'
|
|
23
|
+
scheme: default
|
|
24
|
+
toggle:
|
|
25
|
+
icon: lucide/sun
|
|
26
|
+
name: Switch to dark mode
|
|
27
|
+
- media: '(prefers-color-scheme: dark)'
|
|
28
|
+
scheme: slate
|
|
29
|
+
toggle:
|
|
30
|
+
icon: lucide/moon
|
|
31
|
+
name: Switch to system preference
|
|
32
|
+
markdown_extensions:
|
|
33
|
+
toc:
|
|
34
|
+
permalink: true
|
|
35
|
+
permalink_title: Link to this section
|
|
36
|
+
pymdownx.arithmatex:
|
|
37
|
+
generic: true
|
|
38
|
+
admonition: {}
|
|
39
|
+
pymdownx.details: {}
|
|
40
|
+
pymdownx.highlight: {}
|
|
41
|
+
pymdownx.superfences:
|
|
42
|
+
custom_fences:
|
|
43
|
+
- name: mermaid
|
|
44
|
+
class: mermaid
|
|
45
|
+
format: pymdownx.superfences.fence_code_format
|
|
46
|
+
pymdownx.tabbed:
|
|
47
|
+
alternate_style: true
|
|
48
|
+
plugins:
|
|
49
|
+
llmstxt:
|
|
50
|
+
sections:
|
|
51
|
+
Documentation: ["*.md"]
|
|
52
|
+
search: {}
|
|
53
|
+
mkdocstrings:
|
|
54
|
+
handlers:
|
|
55
|
+
python:
|
|
56
|
+
options:
|
|
57
|
+
docstring_style: google
|
|
58
|
+
members_order: alphabetical
|
|
59
|
+
filters: ["!^_"]
|
|
60
|
+
show_signature_annotations: true
|
|
61
|
+
separate_signature: true
|
|
62
|
+
signature_crossrefs: true
|
|
63
|
+
line_length: 76
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Project-owned workflow: configure creates this once and never overwrites it.
|
|
2
|
+
name: Deploy documentation
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch: # Allow manual deployment.
|
|
6
|
+
|
|
7
|
+
# Uncomment to deploy on pushes to main (for example, consumer projects):
|
|
8
|
+
# push:
|
|
9
|
+
# branches: [main]
|
|
10
|
+
|
|
11
|
+
# Or uncomment to deploy on published releases (for example, BONDI):
|
|
12
|
+
# release:
|
|
13
|
+
# types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
pages: write
|
|
18
|
+
id-token: write
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
docs:
|
|
22
|
+
# Replace @main with a reviewed commit SHA to pin the deployment implementation.
|
|
23
|
+
uses: relaxed-constraints/relcon-docs/.github/workflows/deploy-docs.yml@main
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* Load the pinned renderer only when a page contains mathematical notation. */
|
|
2
|
+
(() => {
|
|
3
|
+
let engine;
|
|
4
|
+
function renderMath() {
|
|
5
|
+
if (!document.querySelector('.arithmatex')) return;
|
|
6
|
+
if (!engine) {
|
|
7
|
+
engine = new Promise((resolve, reject) => {
|
|
8
|
+
window.MathJax = {
|
|
9
|
+
tex: {
|
|
10
|
+
inlineMath: [['\\(', '\\)']],
|
|
11
|
+
displayMath: [['\\[', '\\]']],
|
|
12
|
+
processEscapes: true,
|
|
13
|
+
processEnvironments: true,
|
|
14
|
+
},
|
|
15
|
+
options: { ignoreHtmlClass: '.*|', processHtmlClass: 'arithmatex' },
|
|
16
|
+
startup: { typeset: false },
|
|
17
|
+
};
|
|
18
|
+
const script = document.createElement('script');
|
|
19
|
+
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js';
|
|
20
|
+
script.onload = () => resolve(window.MathJax.startup.promise);
|
|
21
|
+
script.onerror = () => reject(new Error('Could not load the MathJax renderer.'));
|
|
22
|
+
document.head.appendChild(script);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
engine = engine.then(() => window.MathJax.typesetPromise()).catch(console.error);
|
|
26
|
+
}
|
|
27
|
+
if (typeof document$ !== 'undefined') {
|
|
28
|
+
document$.subscribe(renderMath);
|
|
29
|
+
} else if (document.readyState === 'loading') {
|
|
30
|
+
document.addEventListener('DOMContentLoaded', renderMath);
|
|
31
|
+
} else {
|
|
32
|
+
renderMath();
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Button colours are paired for readable labels and visible focus in each theme. */
|
|
2
|
+
:root,
|
|
3
|
+
[data-md-color-scheme="default"] {
|
|
4
|
+
--relcon-copy-bg: #ffffff;
|
|
5
|
+
--relcon-copy-hover: #edf1f5;
|
|
6
|
+
--relcon-copy-border: #7a8796;
|
|
7
|
+
--relcon-copy-text: #263445;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
[data-md-color-scheme="slate"] {
|
|
11
|
+
--relcon-copy-bg: #222b38;
|
|
12
|
+
--relcon-copy-hover: #303e50;
|
|
13
|
+
--relcon-copy-border: #8193aa;
|
|
14
|
+
--relcon-copy-text: #edf2f8;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.md-typeset button[data-md-copy-url] {
|
|
18
|
+
display: inline-flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 0.4em;
|
|
21
|
+
padding: 0.35em 0.65em;
|
|
22
|
+
background-color: var(--relcon-copy-bg);
|
|
23
|
+
border: 1px solid var(--relcon-copy-border);
|
|
24
|
+
color: var(--relcon-copy-text);
|
|
25
|
+
border-radius: 0.25rem;
|
|
26
|
+
font-size: 0.65rem;
|
|
27
|
+
line-height: 1.3;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.md-typeset button[data-md-copy-url]::after {
|
|
31
|
+
content: attr(aria-label);
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.md-typeset button[data-md-copy-url]:focus-visible {
|
|
36
|
+
outline: 2px solid currentColor;
|
|
37
|
+
outline-offset: 2px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Diagram roles inherit into Zensical's Mermaid shadow roots. */
|
|
41
|
+
:root,
|
|
42
|
+
[data-md-color-scheme="default"] {
|
|
43
|
+
--relcon-diagram-frame-bg: #f5f7fa;
|
|
44
|
+
--relcon-diagram-frame-border: #758399;
|
|
45
|
+
--relcon-diagram-node-bg: #e8eff7;
|
|
46
|
+
--relcon-diagram-node-border: #59718f;
|
|
47
|
+
--relcon-diagram-text: #223248;
|
|
48
|
+
--relcon-diagram-edge: #526780;
|
|
49
|
+
--relcon-diagram-accent-bg: #fff0cc;
|
|
50
|
+
--relcon-diagram-accent-border: #a56a12;
|
|
51
|
+
--relcon-diagram-accent-text: #623a00;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[data-md-color-scheme="slate"] {
|
|
55
|
+
--relcon-diagram-frame-bg: #202a38;
|
|
56
|
+
--relcon-diagram-frame-border: #778ba4;
|
|
57
|
+
--relcon-diagram-node-bg: #293c53;
|
|
58
|
+
--relcon-diagram-node-border: #8eaacb;
|
|
59
|
+
--relcon-diagram-text: #eef3f9;
|
|
60
|
+
--relcon-diagram-edge: #94a9c2;
|
|
61
|
+
--relcon-diagram-accent-bg: #513b18;
|
|
62
|
+
--relcon-diagram-accent-border: #e6b65c;
|
|
63
|
+
--relcon-diagram-accent-text: #fff0cd;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:root,
|
|
67
|
+
[data-md-color-scheme] {
|
|
68
|
+
/* Native flowcharts need no per-diagram style declarations. */
|
|
69
|
+
--md-mermaid-node-bg-color: var(--relcon-diagram-node-bg);
|
|
70
|
+
--md-mermaid-node-fg-color: var(--relcon-diagram-node-border);
|
|
71
|
+
--md-mermaid-label-fg-color: var(--relcon-diagram-text);
|
|
72
|
+
--md-mermaid-label-bg-color: var(--md-default-bg-color);
|
|
73
|
+
--md-mermaid-edge-color: var(--relcon-diagram-edge);
|
|
74
|
+
--md-mermaid-sequence-actor-bg-color: var(--relcon-diagram-node-bg);
|
|
75
|
+
--md-mermaid-sequence-actor-border-color: var(--relcon-diagram-node-border);
|
|
76
|
+
--md-mermaid-sequence-actor-fg-color: var(--relcon-diagram-text);
|
|
77
|
+
--md-mermaid-sequence-actor-line-color: var(--relcon-diagram-edge);
|
|
78
|
+
--md-mermaid-sequence-message-fg-color: var(--relcon-diagram-text);
|
|
79
|
+
--md-mermaid-sequence-message-line-color: var(--relcon-diagram-edge);
|
|
80
|
+
--md-mermaid-sequence-note-bg-color: var(--relcon-diagram-frame-bg);
|
|
81
|
+
--md-mermaid-sequence-note-border-color: var(--relcon-diagram-frame-border);
|
|
82
|
+
--md-mermaid-sequence-note-fg-color: var(--relcon-diagram-text);
|
|
83
|
+
|
|
84
|
+
/* Compatibility with diagrams authored before relcon-docs. */
|
|
85
|
+
--jb-frame-bg: var(--relcon-diagram-frame-bg);
|
|
86
|
+
--jb-frame-border: var(--relcon-diagram-frame-border);
|
|
87
|
+
--jb-node-bg: var(--relcon-diagram-node-bg);
|
|
88
|
+
--jb-node-border: var(--relcon-diagram-node-border);
|
|
89
|
+
--jb-diagram-text: var(--relcon-diagram-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.md-typeset button[data-md-copy-url]:hover {
|
|
93
|
+
background-color: var(--relcon-copy-hover);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.md-typeset figure {
|
|
97
|
+
margin: 1.5em 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.md-typeset figure img {
|
|
101
|
+
display: block;
|
|
102
|
+
max-width: 100%;
|
|
103
|
+
height: auto;
|
|
104
|
+
margin-inline: auto;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.md-typeset figcaption {
|
|
108
|
+
margin-top: 0.75em;
|
|
109
|
+
color: var(--md-default-fg-color--light);
|
|
110
|
+
text-align: center;
|
|
111
|
+
}
|
relcon_docs/cli.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Apply packaged defaults without rewriting project-owned configuration."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import difflib
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
BASE = ".relcon-docs/base.yml"
|
|
17
|
+
STATE = ".relcon-docs/state.json"
|
|
18
|
+
CSS = "docs/stylesheets/relcon-docs.css"
|
|
19
|
+
WORKFLOW = ".github/workflows/docs.yml"
|
|
20
|
+
MATH = "docs/javascripts/relcon-docs-math.js"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigurationError(Exception):
|
|
24
|
+
"""A configuration cannot safely be applied."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def digest(content: bytes) -> str:
|
|
28
|
+
return hashlib.sha256(content).hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def managed_assets() -> dict[str, bytes]:
|
|
32
|
+
assets = files("relcon_docs").joinpath("assets")
|
|
33
|
+
return {
|
|
34
|
+
BASE: assets.joinpath("base.yml").read_bytes(),
|
|
35
|
+
CSS: assets.joinpath("relcon-docs.css").read_bytes(),
|
|
36
|
+
MATH: assets.joinpath("relcon-docs-math.js").read_bytes(),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def safe_path(root: Path, relative: str) -> Path:
|
|
41
|
+
"""Reject symlinks and non-directory parents before reading or writing."""
|
|
42
|
+
path = root
|
|
43
|
+
for part in Path(relative).parts:
|
|
44
|
+
path = path / part
|
|
45
|
+
if path.is_symlink():
|
|
46
|
+
raise ConfigurationError(f"Refusing symlink: {path}")
|
|
47
|
+
if path != root / relative and path.exists() and not path.is_dir():
|
|
48
|
+
raise ConfigurationError(f"Expected a directory: {path}")
|
|
49
|
+
if path.exists() and not path.is_file():
|
|
50
|
+
raise ConfigurationError(f"Expected a file: {path}")
|
|
51
|
+
return path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_state(path: Path, assets: dict[str, bytes]) -> dict[str, str]:
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return {}
|
|
57
|
+
try:
|
|
58
|
+
state = json.loads(path.read_text())
|
|
59
|
+
hashes = state["files"]
|
|
60
|
+
if (
|
|
61
|
+
state["schema"] != 1
|
|
62
|
+
or not isinstance(hashes, dict)
|
|
63
|
+
or set(hashes) not in ({BASE, CSS}, set(assets))
|
|
64
|
+
or not all(
|
|
65
|
+
isinstance(value, str)
|
|
66
|
+
and len(value) == 64
|
|
67
|
+
and all(char in "0123456789abcdef" for char in value)
|
|
68
|
+
for value in hashes.values()
|
|
69
|
+
)
|
|
70
|
+
):
|
|
71
|
+
raise ValueError
|
|
72
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
73
|
+
raise ConfigurationError(
|
|
74
|
+
f"Invalid state file: {path}. Restore it from Git before configuring."
|
|
75
|
+
) from exc
|
|
76
|
+
return hashes
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def project_config(root: Path) -> bytes | None:
|
|
80
|
+
"""Validate an existing project or prepare a starter, without editing it."""
|
|
81
|
+
if (root / "zensical.toml").exists():
|
|
82
|
+
raise ConfigurationError("Convert zensical.toml to mkdocs.yml first; it takes precedence.")
|
|
83
|
+
if (root / "mkdocs.yaml").exists():
|
|
84
|
+
raise ConfigurationError("Rename mkdocs.yaml to mkdocs.yml first.")
|
|
85
|
+
path = safe_path(root, "mkdocs.yml")
|
|
86
|
+
if not path.exists():
|
|
87
|
+
return (
|
|
88
|
+
"# Project-owned configuration. Override shared defaults here.\n"
|
|
89
|
+
"# Replace the local site_url with your published URL before deployment.\n"
|
|
90
|
+
f"INHERIT: {BASE}\n"
|
|
91
|
+
+ yaml.safe_dump(
|
|
92
|
+
{
|
|
93
|
+
"site_name": root.name,
|
|
94
|
+
"site_url": "http://127.0.0.1:8000/",
|
|
95
|
+
},
|
|
96
|
+
sort_keys=False,
|
|
97
|
+
)
|
|
98
|
+
).encode()
|
|
99
|
+
|
|
100
|
+
# Compose rather than construct: existing MkDocs configurations may use
|
|
101
|
+
# !ENV and Python-name tags, which we must neither execute nor rewrite.
|
|
102
|
+
try:
|
|
103
|
+
config = yaml.compose(path.read_text())
|
|
104
|
+
if not isinstance(config, yaml.MappingNode):
|
|
105
|
+
raise ValueError
|
|
106
|
+
settings = {}
|
|
107
|
+
for key, value in config.value:
|
|
108
|
+
if not isinstance(key, yaml.ScalarNode) or key.value in settings:
|
|
109
|
+
raise ValueError
|
|
110
|
+
settings[key.value] = value
|
|
111
|
+
inherit = settings.get("INHERIT")
|
|
112
|
+
if not isinstance(inherit, yaml.ScalarNode) or inherit.value != BASE:
|
|
113
|
+
raise ConfigurationError(f"Set INHERIT: {BASE} in mkdocs.yml first.")
|
|
114
|
+
docs_dir = settings.get("docs_dir")
|
|
115
|
+
if docs_dir is not None and (
|
|
116
|
+
not isinstance(docs_dir, yaml.ScalarNode) or docs_dir.value not in ("docs", "./docs")
|
|
117
|
+
):
|
|
118
|
+
raise ConfigurationError("This version supports only docs_dir: docs.")
|
|
119
|
+
except (yaml.YAMLError, ValueError) as exc:
|
|
120
|
+
raise ConfigurationError("mkdocs.yml must be a YAML mapping with unique keys.") from exc
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def atomic_write(path: Path, content: bytes) -> None:
|
|
125
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
temporary = None
|
|
127
|
+
try:
|
|
128
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as stream:
|
|
129
|
+
temporary = Path(stream.name)
|
|
130
|
+
stream.write(content)
|
|
131
|
+
os.chmod(temporary, 0o644)
|
|
132
|
+
os.replace(temporary, path)
|
|
133
|
+
finally:
|
|
134
|
+
if temporary is not None:
|
|
135
|
+
temporary.unlink(missing_ok=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def configure(root: Path, *, dry_run: bool, check: bool, force: bool) -> int:
|
|
139
|
+
if not root.is_dir():
|
|
140
|
+
raise ConfigurationError(f"Project directory does not exist: {root}")
|
|
141
|
+
assets = managed_assets()
|
|
142
|
+
paths = {name: safe_path(root, name) for name in [*assets, STATE]}
|
|
143
|
+
baseline = read_state(paths[STATE], assets)
|
|
144
|
+
starter = project_config(root)
|
|
145
|
+
workflow_path = safe_path(root, WORKFLOW)
|
|
146
|
+
changes = {}
|
|
147
|
+
conflicts = []
|
|
148
|
+
|
|
149
|
+
for name, desired in assets.items():
|
|
150
|
+
path = paths[name]
|
|
151
|
+
current = path.read_bytes() if path.exists() else None
|
|
152
|
+
if current == desired:
|
|
153
|
+
continue
|
|
154
|
+
changes[name] = desired
|
|
155
|
+
if name in baseline:
|
|
156
|
+
if current is None or digest(current) != baseline[name]:
|
|
157
|
+
conflicts.append(name)
|
|
158
|
+
elif current is not None:
|
|
159
|
+
conflicts.append(name)
|
|
160
|
+
|
|
161
|
+
if starter is not None:
|
|
162
|
+
changes["mkdocs.yml"] = starter
|
|
163
|
+
if not workflow_path.exists():
|
|
164
|
+
changes[WORKFLOW] = files("relcon_docs").joinpath("assets/docs.yml").read_bytes()
|
|
165
|
+
state = {
|
|
166
|
+
"schema": 1,
|
|
167
|
+
"package_version": version("relcon-docs"),
|
|
168
|
+
"files": {name: digest(content) for name, content in assets.items()},
|
|
169
|
+
}
|
|
170
|
+
state_bytes = (json.dumps(state, indent=2, sort_keys=True) + "\n").encode()
|
|
171
|
+
if not paths[STATE].exists() or paths[STATE].read_bytes() != state_bytes:
|
|
172
|
+
changes[STATE] = state_bytes
|
|
173
|
+
|
|
174
|
+
for name in changes:
|
|
175
|
+
print(f"Update {name}" if (root / name).exists() else f"Create {name}")
|
|
176
|
+
if dry_run:
|
|
177
|
+
path = root / name
|
|
178
|
+
before = path.read_bytes() if path.exists() else b""
|
|
179
|
+
print(
|
|
180
|
+
"".join(
|
|
181
|
+
difflib.unified_diff(
|
|
182
|
+
before.decode(errors="replace").splitlines(keepends=True),
|
|
183
|
+
changes[name].decode().splitlines(keepends=True),
|
|
184
|
+
fromfile=f"a/{name}",
|
|
185
|
+
tofile=f"b/{name}",
|
|
186
|
+
)
|
|
187
|
+
),
|
|
188
|
+
end="",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if conflicts:
|
|
192
|
+
print("Managed-file conflicts: " + ", ".join(conflicts), file=sys.stderr)
|
|
193
|
+
if not force:
|
|
194
|
+
print(
|
|
195
|
+
"No files written. Move customizations to mkdocs.yml or a separate CSS file; "
|
|
196
|
+
"use --force to replace managed files.",
|
|
197
|
+
file=sys.stderr,
|
|
198
|
+
)
|
|
199
|
+
return 2
|
|
200
|
+
print("Explicit --force: package contents will replace these managed files.")
|
|
201
|
+
|
|
202
|
+
if check:
|
|
203
|
+
if not changes:
|
|
204
|
+
print("Configuration is current.")
|
|
205
|
+
return 1 if changes else 0
|
|
206
|
+
if dry_run:
|
|
207
|
+
print("Dry run: no files written.")
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
# Validate all destinations before writing anything. Each file replacement
|
|
211
|
+
# is atomic; the state is written last so an interrupted run can be retried.
|
|
212
|
+
for name in changes:
|
|
213
|
+
safe_path(root, name)
|
|
214
|
+
for name, content in changes.items():
|
|
215
|
+
atomic_write(root / name, content)
|
|
216
|
+
print("Configuration updated." if changes else "Configuration is current.")
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main(argv: list[str] | None = None) -> int:
|
|
221
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
222
|
+
parser.add_argument("--version", action="version", version=version("relcon-docs"))
|
|
223
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
224
|
+
command = commands.add_parser("configure", help="Apply shared documentation defaults")
|
|
225
|
+
command.add_argument(
|
|
226
|
+
"--project", type=Path, default=Path.cwd(), help="Project root (default: cwd)"
|
|
227
|
+
)
|
|
228
|
+
mode = command.add_mutually_exclusive_group()
|
|
229
|
+
mode.add_argument("--dry-run", action="store_true", help="Show diffs without writing")
|
|
230
|
+
mode.add_argument(
|
|
231
|
+
"--check", action="store_true", help="Exit 1 if updates are needed; do not write"
|
|
232
|
+
)
|
|
233
|
+
command.add_argument("--force", action="store_true", help="Replace conflicting managed files")
|
|
234
|
+
args = parser.parse_args(argv)
|
|
235
|
+
if args.check and args.force:
|
|
236
|
+
parser.error("--check cannot be combined with --force")
|
|
237
|
+
try:
|
|
238
|
+
return configure(
|
|
239
|
+
args.project.resolve(), dry_run=args.dry_run, check=args.check, force=args.force
|
|
240
|
+
)
|
|
241
|
+
except (ConfigurationError, OSError) as exc:
|
|
242
|
+
print(f"relcon-docs: {exc}", file=sys.stderr)
|
|
243
|
+
return 2
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: relcon-docs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared documentation defaults, styling, and configuration tools for Zensical
|
|
5
|
+
Project-URL: Homepage, https://relaxed-constraints.github.io/relcon-docs/
|
|
6
|
+
Project-URL: Repository, https://github.com/relaxed-constraints/relcon-docs
|
|
7
|
+
Project-URL: Issues, https://github.com/relaxed-constraints/relcon-docs/issues
|
|
8
|
+
Author: Relaxed Constraints
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: mkdocstrings-python>=2.0.10
|
|
13
|
+
Requires-Dist: pyyaml<7,>=6
|
|
14
|
+
Requires-Dist: ruff>=0.9
|
|
15
|
+
Requires-Dist: zensical<0.2,>=0.1.1
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# relcon-docs
|
|
19
|
+
|
|
20
|
+
Shared documentation defaults, styling, and configuration tools for Relaxed Constraints
|
|
21
|
+
projects using Zensical.
|
|
22
|
+
|
|
23
|
+
Install this package as a **documentation dependency**. It supplies Zensical, the
|
|
24
|
+
Python API handler and formatter, shared modern-theme settings, and copy-button CSS.
|
|
25
|
+
It has no dependency on BONDI or Yanex.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv add --group docs relcon-docs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install with `python -m pip install relcon-docs`. Keep uv.lock committed for
|
|
34
|
+
reproducible builds. Upgrade with `uv lock --upgrade-package relcon-docs`, sync
|
|
35
|
+
the docs group, and run `relcon-docs configure` to refresh shared assets.
|
|
36
|
+
|
|
37
|
+
## Configure a project
|
|
38
|
+
|
|
39
|
+
Once installed in your project's environment:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
relcon-docs configure --dry-run
|
|
43
|
+
relcon-docs configure
|
|
44
|
+
zensical serve
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The project directory must already exist. A fresh project gets a minimal
|
|
48
|
+
`mkdocs.yml`; add your Markdown pages under `docs/` and edit navigation as needed.
|
|
49
|
+
The starter uses a localhost `site_url` for previews. Replace it with the published
|
|
50
|
+
site URL before deployment; Markdown exports require this setting.
|
|
51
|
+
For an existing YAML project, add `INHERIT: .relcon-docs/base.yml` first.
|
|
52
|
+
Convert and remove any `zensical.toml` before configuring: Zensical prefers it over YAML.
|
|
53
|
+
|
|
54
|
+
| File | Owner |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `.github/workflows/docs.yml` | Project: deployment triggers and reusable workflow revision |
|
|
57
|
+
| `mkdocs.yml` | Project: metadata, navigation, API paths, Markdown-export sections and overrides |
|
|
58
|
+
| `.relcon-docs/base.yml` | Package: theme, Markdown extensions and API-rendering defaults |
|
|
59
|
+
| `docs/stylesheets/relcon-docs.css` | Package: copy buttons, diagrams and figures |
|
|
60
|
+
| `docs/javascripts/relcon-docs-math.js` | Package: on-demand MathJax rendering |
|
|
61
|
+
| `.relcon-docs/state.json` | Tool: applied version and hashes for detecting manual edits |
|
|
62
|
+
|
|
63
|
+
Commit these files. Configuration is explicit: installing or upgrading the
|
|
64
|
+
dependency never rewrites project files. Builds and previews use ordinary
|
|
65
|
+
`zensical` commands, without a wrapper or sibling checkout.
|
|
66
|
+
|
|
67
|
+
## Upgrade and review
|
|
68
|
+
|
|
69
|
+
Upgrade the dependency and lockfile, then run:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
relcon-docs configure --dry-run
|
|
73
|
+
relcon-docs configure
|
|
74
|
+
zensical build --strict
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Unmodified managed files are updated. A changed or deleted managed file produces a
|
|
78
|
+
conflict and **no files are written**. On first adoption, an existing managed path
|
|
79
|
+
with different contents also conflicts. Identical contents can be adopted safely.
|
|
80
|
+
|
|
81
|
+
Move customizations into project overrides or a separate stylesheet. To explicitly
|
|
82
|
+
replace conflicting managed files, use `configure --force`; inspect the proposed
|
|
83
|
+
changes first with `configure --dry-run --force`. Force never rewrites an existing
|
|
84
|
+
`mkdocs.yml` or `.github/workflows/docs.yml`. Invalid state files must be restored from Git; force does not bypass
|
|
85
|
+
state validation or symlink protection.
|
|
86
|
+
|
|
87
|
+
Each file replacement is atomic; the full set is not a filesystem transaction.
|
|
88
|
+
State is written last, and an interrupted run can be retried.
|
|
89
|
+
|
|
90
|
+
## Project overrides
|
|
91
|
+
|
|
92
|
+
Mappings merge recursively. Lists replace inherited lists, so repeat the shared
|
|
93
|
+
entries when overriding `theme.features` or `extra_css`. Use mapping syntax for
|
|
94
|
+
plugins and Markdown extensions to preserve unrelated inherited defaults:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
INHERIT: .relcon-docs/base.yml
|
|
98
|
+
site_name: My research project
|
|
99
|
+
site_url: https://example.org/my-project/
|
|
100
|
+
plugins:
|
|
101
|
+
mkdocstrings:
|
|
102
|
+
handlers:
|
|
103
|
+
python:
|
|
104
|
+
paths: [src]
|
|
105
|
+
llmstxt:
|
|
106
|
+
sections:
|
|
107
|
+
Documentation: ["*.md"]
|
|
108
|
+
API reference: ["reference/**/*.md"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Markdown exports and `llms.txt` are enabled in the shared defaults for all
|
|
112
|
+
Markdown pages, including nested pages. Existing projects inherit them without
|
|
113
|
+
adding a local plugin configuration. A valid project `site_url` is required.
|
|
114
|
+
|
|
115
|
+
The shared index uses a `Documentation` section. Section mappings merge during
|
|
116
|
+
inheritance: override `Documentation` to change its page patterns; adding a
|
|
117
|
+
different section name retains the inherited section too. Most projects can omit
|
|
118
|
+
`llmstxt` entirely. Set `plugins.llmstxt.enabled: false` to opt out.
|
|
119
|
+
|
|
120
|
+
This first version supports the standard `docs/` source directory and direct
|
|
121
|
+
inheritance from `.relcon-docs/base.yml`. It does not migrate arbitrary configurations,
|
|
122
|
+
prune old stylesheets, or modify existing workflow files.
|
|
123
|
+
|
|
124
|
+
## Callouts, collapsible notes and diagrams
|
|
125
|
+
|
|
126
|
+
Shared defaults enable `admonition`, `pymdownx.details` and Mermaid fenced
|
|
127
|
+
blocks. Projects do not need local extension configuration for these features.
|
|
128
|
+
For example:
|
|
129
|
+
|
|
130
|
+
````markdown
|
|
131
|
+
!!! note "Research note"
|
|
132
|
+
|
|
133
|
+
Record assumptions beside your results.
|
|
134
|
+
|
|
135
|
+
??? tip "Show details"
|
|
136
|
+
|
|
137
|
+
Put optional explanations here.
|
|
138
|
+
|
|
139
|
+
```mermaid
|
|
140
|
+
graph LR
|
|
141
|
+
Data --> Experiment --> Results
|
|
142
|
+
```
|
|
143
|
+
````
|
|
144
|
+
|
|
145
|
+
Zensical initializes Mermaid on pages containing diagrams. Shared light/dark colours apply to standard flowchart nodes, labels and connectors,
|
|
146
|
+
and sequence actors, messages and notes. Custom diagrams can use the
|
|
147
|
+
`--relcon-diagram-*` CSS properties; legacy `--jb-*` aliases remain supported.
|
|
148
|
+
See the [diagram examples](https://relaxed-constraints.github.io/relcon-docs/diagrams/).
|
|
149
|
+
Project-specific overrides can remain in a separate local stylesheet.
|
|
150
|
+
If overriding `custom_fences`, include the Mermaid entry too: lists replace
|
|
151
|
+
inherited lists.
|
|
152
|
+
|
|
153
|
+
## GitHub Pages deployment
|
|
154
|
+
|
|
155
|
+
`configure` creates `.github/workflows/docs.yml` **only when absent**. The starter
|
|
156
|
+
enables manual deployment (`workflow_dispatch`) and includes commented examples
|
|
157
|
+
for pushes to `main` and published releases. After creation the file belongs to the
|
|
158
|
+
project: rerunning configure, including with `--force`, leaves it byte-for-byte
|
|
159
|
+
unchanged. It is not included in the managed-file hashes. A missing starter is
|
|
160
|
+
reported by `--check` and shown by `--dry-run`; ordinary configure recreates it.
|
|
161
|
+
|
|
162
|
+
The starter calls the shared
|
|
163
|
+
[`deploy-docs.yml`](https://github.com/relaxed-constraints/relcon-docs/blob/main/.github/workflows/deploy-docs.yml)
|
|
164
|
+
workflow. It checks out the **caller repository and triggering revision**, installs
|
|
165
|
+
its locked `docs` dependency group, checks managed configuration, runs a strict
|
|
166
|
+
Zensical build, uploads `site/`, and deploys it to GitHub Pages. It does not update
|
|
167
|
+
configuration during deployment.
|
|
168
|
+
|
|
169
|
+
Choose the project's policy by editing the starter's `on` mapping. For BONDI,
|
|
170
|
+
keep automatic deployment limited to published releases:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
on:
|
|
174
|
+
workflow_dispatch:
|
|
175
|
+
release:
|
|
176
|
+
types: [published]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A consumer project can instead publish on each push to `main`:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
on:
|
|
183
|
+
workflow_dispatch:
|
|
184
|
+
push:
|
|
185
|
+
branches: [main]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The caller grants `contents: read`, `pages: write` and `id-token: write`.
|
|
189
|
+
The build job uses read-only repository access; the deployment job gets Pages
|
|
190
|
+
permissions and uses the `github-pages` environment. Deployments share a concurrency
|
|
191
|
+
group and do not cancel an in-progress deployment.
|
|
192
|
+
|
|
193
|
+
### Repository setup and workflow updates
|
|
194
|
+
|
|
195
|
+
- Set **Settings → Pages → Source → GitHub Actions** once in each repository.
|
|
196
|
+
- Set the project's `site_url` to its published URL before deploying.
|
|
197
|
+
- Commit `uv.lock`, provide a `docs` dependency group, and use the standard
|
|
198
|
+
`docs/` source and `site/` output directories.
|
|
199
|
+
- For release deployment, allow the release tags under the `github-pages`
|
|
200
|
+
environment's deployment protection rules.
|
|
201
|
+
- When an existing deployment workflow uses another name (such as `pages.yml`),
|
|
202
|
+
consolidate it with the starter before enabling automatic triggers. Configure
|
|
203
|
+
does not remove or alter other workflows.
|
|
204
|
+
|
|
205
|
+
The starter references `@main`; use a reviewed full commit SHA for reproducible
|
|
206
|
+
deployment tooling. No `v1` tag is assumed. The workflow revision and Python package
|
|
207
|
+
dependency are independent: upgrading the package never rewrites the caller's
|
|
208
|
+
workflow reference or triggers.
|
|
209
|
+
|
|
210
|
+
Optional caller job inputs are `python-version` (default `"3.11"`) and
|
|
211
|
+
`uv-version` (default `"0.13.6"`). For example:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
jobs:
|
|
215
|
+
docs:
|
|
216
|
+
uses: relaxed-constraints/relcon-docs/.github/workflows/deploy-docs.yml@main
|
|
217
|
+
with:
|
|
218
|
+
python-version: "3.12"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## CLI reference
|
|
222
|
+
|
|
223
|
+
`relcon-docs configure [--project PATH] [--dry-run | --check] [--force]`
|
|
224
|
+
|
|
225
|
+
- `--project`: target an existing project root; defaults to the working directory.
|
|
226
|
+
- `--dry-run`: print proposed file diffs without writing. Conflicts still fail unless forced.
|
|
227
|
+
- `--check`: write nothing; exit 0 when current, 1 for pending updates, 2 for errors/conflicts.
|
|
228
|
+
- `--force`: explicitly replace conflicting managed files; incompatible with `--check`.
|
|
229
|
+
- `relcon-docs --version`: display the installed package version.
|
|
230
|
+
|
|
231
|
+
Use `relcon-docs configure --check` in CI before `zensical build --strict`.
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
uv sync --locked
|
|
237
|
+
uv run pytest
|
|
238
|
+
uv run ruff check .
|
|
239
|
+
uv run ruff format --check .
|
|
240
|
+
uv run relcon-docs configure --check
|
|
241
|
+
uv run zensical build --strict
|
|
242
|
+
uv build
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
For a local consumer trial before publishing, install the built wheel in an isolated
|
|
246
|
+
environment. The installed package contains all assets; consumers need no checkout
|
|
247
|
+
of this repository.
|
|
248
|
+
|
|
249
|
+
## Feature examples
|
|
250
|
+
|
|
251
|
+
The documentation includes walkthroughs for Markdown, Mermaid diagrams, equations
|
|
252
|
+
and figures. Equations use the shared Arithmatex configuration and a pinned
|
|
253
|
+
MathJax renderer loaded from jsDelivr only on pages containing mathematics.
|
|
254
|
+
The site itself uses relcon-docs and its reusable deployment workflow.
|
|
255
|
+
|
|
256
|
+
### API member defaults
|
|
257
|
+
|
|
258
|
+
API members are ordered alphabetically. Members whose names start with `_` are
|
|
259
|
+
excluded from automatic member listings, including double-underscore names.
|
|
260
|
+
Projects can override `members_order` and `filters` under
|
|
261
|
+
`plugins.mkdocstrings.handlers.python.options` in `mkdocs.yml`, or on an
|
|
262
|
+
individual API directive when a specific private or special method needs documenting.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
relcon_docs/__init__.py,sha256=W0NXnT-yLV2EiR47avbXl4Wc0z0TVreVpty1L55oENs,75
|
|
2
|
+
relcon_docs/cli.py,sha256=9p-VR4ZVCQpyxkqN2VyVFr8Zax_3A5hGE_4L-gJReK4,9201
|
|
3
|
+
relcon_docs/assets/base.yml,sha256=62HfufJqTk2Zjwq-2IP7186KvxLVMfoWE134Ja2uOck,1542
|
|
4
|
+
relcon_docs/assets/docs.yml,sha256=eGpQdIndm2jeVWazXyuRDHZqFgEQxsyVziuxCk5dWCk,624
|
|
5
|
+
relcon_docs/assets/relcon-docs-math.js,sha256=uOexX0GfTlIINXASL1N-pwNrtSklbX8XKq5RRJ1SNmE,1284
|
|
6
|
+
relcon_docs/assets/relcon-docs.css,sha256=r7xrqQrU6A6deb3YmkqCWsL_Yq9MIT0ij2eL6HyBCoU,3520
|
|
7
|
+
relcon_docs-0.1.0.dist-info/METADATA,sha256=RVken39weXKGH7djUsVHlsoJVsSauUbD6tZ1Btb_YtI,10364
|
|
8
|
+
relcon_docs-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
9
|
+
relcon_docs-0.1.0.dist-info/entry_points.txt,sha256=ay_WbSM4YntoyOebqfHlmKovXjpjX5TEauCAmoK_xoo,53
|
|
10
|
+
relcon_docs-0.1.0.dist-info/licenses/LICENSE,sha256=YboKyi6DcoknxJ9-nAyx50FWQP1Bi7fpBbDGP8IZRcc,1076
|
|
11
|
+
relcon_docs-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Relaxed Constraints
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|