Skip to content

Commit 598c2ba

Browse files
committed
feat(docs): add versioning support for documentation
- Introduced versioning for documentation by creating a new `versions.js` file that holds available versions. - Updated `docs.js` to dynamically load documentation based on the selected version from the URL query parameter. - Enhanced the documentation conversion tool to accept a version argument, generating versioned output files and updating `versions.js` accordingly. - Improved user experience by adding a version selector in the documentation viewer.
1 parent 309d172 commit 598c2ba

7 files changed

Lines changed: 423 additions & 15 deletions

File tree

css/docs.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@
22
.docs-layout {
33
display: flex;
44
min-height: calc(100vh - var(--header-height));
5+
max-width: 1440px;
6+
margin: 0 auto;
7+
width: 100%;
8+
}
9+
10+
/* ===== Sidebar Version Selector ===== */
11+
.sidebar-version-selector {
12+
padding: 12px 20px 16px;
13+
border-bottom: 1px solid var(--color-border-light);
14+
margin-bottom: 8px;
15+
}
16+
17+
.sidebar-version-selector label {
18+
display: block;
19+
font-size: 0.72rem;
20+
font-weight: 600;
21+
color: var(--color-text-muted);
22+
text-transform: uppercase;
23+
letter-spacing: 0.06em;
24+
margin-bottom: 6px;
25+
}
26+
27+
.sidebar-version-selector select {
28+
width: 100%;
29+
padding: 6px 10px;
30+
border: 1px solid var(--color-border);
31+
border-radius: var(--radius-sm);
32+
background: var(--color-bg);
33+
color: var(--color-text);
34+
font-size: 0.88rem;
35+
font-family: inherit;
36+
cursor: pointer;
37+
}
38+
39+
.sidebar-version-selector select:focus {
40+
outline: 2px solid var(--color-primary);
41+
outline-offset: 1px;
542
}
643

744
/* ===== Sidebar ===== */
@@ -116,6 +153,7 @@
116153
min-width: 0;
117154
padding: 40px 48px;
118155
max-width: 900px;
156+
margin: 0 auto;
119157
counter-reset: doc-ol;
120158
}
121159

docs.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@
3939
<div class="docs-layout">
4040
<!-- ===== Sidebar ===== -->
4141
<aside class="docs-sidebar" id="docsSidebar">
42+
<!-- Version selector (hidden until versions.js provides data) -->
43+
<div class="sidebar-version-selector" id="versionWrapper" style="display:none">
44+
<label for="versionSelect">Version</label>
45+
<select id="versionSelect"></select>
46+
</div>
4247
<ul class="sidebar-nav" id="sidebarNav">
43-
<!-- Populated by docs.js from docs-data.js -->
48+
<!-- Populated by docs.js -->
4449
</ul>
4550
</aside>
4651

@@ -67,7 +72,7 @@
6772
<p>&copy; 2022–2026 PHPER Framework Team · Licensed under <a href="https://github.com/phper-framework/phper/blob/master/LICENSE" target="_blank" rel="noopener">MulanPSL-2.0</a></p>
6873
</footer>
6974

70-
<script src="docs/docs-data.js"></script>
75+
<script src="docs/versions.js"></script>
7176
<script src="js/docs.js"></script>
7277
</body>
7378
</html>

docs/0.16/docs-data.js

Lines changed: 121 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/0.17/docs-data.js

Lines changed: 121 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/versions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Auto-generated by tools/convert-docs.py — DO NOT EDIT
2+
window.DOCS_VERSIONS = ["0.17", "0.16"];

js/docs.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,70 @@
11
// PHPER Docs — Documentation viewer with sidebar navigation
2-
// Reads DOCS_DATA from docs/docs-data.js and renders documentation pages
2+
// Supports versioned docs data: docs/{version}/docs-data.js
3+
// Falls back to docs/docs-data.js when no version is specified
34

4-
document.addEventListener('DOMContentLoaded', () => {
5+
/** Return the currently selected version from URL query param, or first in DOCS_VERSIONS. */
6+
function getVersion() {
7+
const params = new URLSearchParams(window.location.search);
8+
const v = params.get('version');
9+
if (v) return v;
10+
if (window.DOCS_VERSIONS && DOCS_VERSIONS.length > 0) return DOCS_VERSIONS[0];
11+
return null;
12+
}
13+
14+
/** Dynamically append a <script> and resolve when loaded. */
15+
function loadScript(src) {
16+
return new Promise((resolve, reject) => {
17+
const s = document.createElement('script');
18+
s.src = src;
19+
s.onload = resolve;
20+
s.onerror = reject;
21+
document.head.appendChild(s);
22+
});
23+
}
24+
25+
document.addEventListener('DOMContentLoaded', async () => {
526
const sidebarNav = document.getElementById('sidebarNav');
627
const docsContent = document.getElementById('docsContent');
728
const sidebarToggle = document.getElementById('sidebarToggle');
829
const sidebar = document.getElementById('docsSidebar');
930
const overlay = document.getElementById('sidebarOverlay');
31+
const versionSelect = document.getElementById('versionSelect');
32+
const versionWrapper = document.getElementById('versionWrapper');
33+
34+
// ── Load versioned (or default) docs data ──────────────────────
35+
const version = getVersion();
36+
try {
37+
if (version) {
38+
await loadScript(`docs/${version}/docs-data.js`);
39+
} else {
40+
await loadScript('docs/docs-data.js');
41+
}
42+
} catch (_) {
43+
// Try fallback
44+
try { await loadScript('docs/docs-data.js'); } catch (e2) { /* ignore */ }
45+
}
46+
47+
// ── Populate version selector ───────────────────────────────────
48+
if (versionSelect && window.DOCS_VERSIONS && DOCS_VERSIONS.length > 0) {
49+
versionWrapper.style.display = '';
50+
DOCS_VERSIONS.forEach((v) => {
51+
const opt = document.createElement('option');
52+
opt.value = v;
53+
opt.textContent = v;
54+
if (v === version) opt.selected = true;
55+
versionSelect.appendChild(opt);
56+
});
57+
versionSelect.addEventListener('change', () => {
58+
const url = new URL(window.location.href);
59+
url.searchParams.set('version', versionSelect.value);
60+
window.location.href = url.toString();
61+
});
62+
}
1063

1164
if (!window.DOCS_DATA || !DOCS_DATA.length) {
1265
docsContent.innerHTML =
1366
'<h1>Documentation</h1><p>No documentation data found. Run the conversion tool first:</p>' +
14-
'<pre><code>python3 tools/convert-docs.py ../phper/phper-doc/doc</code></pre>';
67+
'<pre><code>python3 tools/convert-docs.py --version 0.5.0 ../phper/phper-doc/doc</code></pre>';
1568
return;
1669
}
1770

tools/convert-docs.py

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@
44
PHPER GitHub Pages site.
55
66
Usage:
7-
python3 tools/convert-docs.py [PHPER_DOC_DIR] [OUTPUT_FILE]
7+
python3 tools/convert-docs.py [PHPER_DOC_DIR] [--version VERSION] [--output OUTPUT]
88
99
PHPER_DOC_DIR Path to phper/phper-doc/doc directory
1010
(default: ../phper/phper-doc/doc)
11-
OUTPUT_FILE Output JS file path
12-
(default: docs/docs-data.js)
11+
--version, -v Version string (e.g. 0.5.0). Output goes to
12+
docs/VERSION/docs-data.js and docs/versions.js is updated.
13+
--output, -o Override output JS file path.
1314
14-
The tool scans the phper-doc directory structure, reads each index.md,
15-
converts Markdown to HTML, and writes a single JS data file that the
16-
docs page can consume.
15+
When --version is supplied the generated file is placed under a versioned
16+
subdirectory (docs/VERSION/docs-data.js) and docs/versions.js is kept in sync
17+
so the docs page can offer a version dropdown selector.
1718
1819
Requirements:
1920
pip install markdown pygments
2021
"""
2122

23+
import argparse
2224
import json
2325
import os
2426
import re
@@ -216,21 +218,82 @@ def write_docs_data(sections: list, output_path: str) -> None:
216218
print(f" {len(sections)} sections, {page_count} total pages")
217219

218220

221+
def update_versions_js(version: str, site_root: Path) -> None:
222+
"""Add *version* to docs/versions.js (newest first), creating the file if needed."""
223+
versions_path = site_root / "docs" / "versions.js"
224+
225+
existing: list = []
226+
if versions_path.exists():
227+
content = versions_path.read_text(encoding="utf-8")
228+
match = re.search(r"window\.DOCS_VERSIONS\s*=\s*(\[.*?\])", content, re.DOTALL)
229+
if match:
230+
try:
231+
existing = json.loads(match.group(1))
232+
except json.JSONDecodeError:
233+
existing = []
234+
235+
if version not in existing:
236+
existing.insert(0, version)
237+
238+
js_content = (
239+
"// Auto-generated by tools/convert-docs.py — DO NOT EDIT\n"
240+
f"window.DOCS_VERSIONS = {json.dumps(existing)};\n"
241+
)
242+
with open(versions_path, "w", encoding="utf-8") as f:
243+
f.write(js_content)
244+
245+
print(f"✓ Updated {versions_path} versions: {existing}")
246+
247+
219248
def main():
220249
# Determine base directory of this script for default paths
221250
script_dir = Path(__file__).resolve().parent
222251
site_root = script_dir.parent
223252

224253
# Default phper-doc path: assume phper repo is sibling
225254
default_doc_dir = site_root.parent / "phper" / "phper-doc" / "doc"
226-
default_output = site_root / "docs" / "docs-data.js"
227255

228-
doc_dir = sys.argv[1] if len(sys.argv) > 1 else str(default_doc_dir)
229-
output = sys.argv[2] if len(sys.argv) > 2 else str(default_output)
256+
parser = argparse.ArgumentParser(
257+
description="Convert phper-doc markdown files into docs-data.js"
258+
)
259+
parser.add_argument(
260+
"doc_dir",
261+
nargs="?",
262+
default=str(default_doc_dir),
263+
metavar="PHPER_DOC_DIR",
264+
help=f"Path to phper/phper-doc/doc directory (default: {default_doc_dir})",
265+
)
266+
parser.add_argument(
267+
"--version",
268+
"-v",
269+
default=None,
270+
metavar="VERSION",
271+
help="Version string, e.g. 0.5.0. Output goes to docs/VERSION/docs-data.js "
272+
"and docs/versions.js is updated.",
273+
)
274+
parser.add_argument(
275+
"--output",
276+
"-o",
277+
default=None,
278+
metavar="OUTPUT_FILE",
279+
help="Override the output JS file path.",
280+
)
230281

231-
print(f"PHPER Documentation Converter")
282+
args = parser.parse_args()
283+
doc_dir = args.doc_dir
284+
285+
if args.output:
286+
output = args.output
287+
elif args.version:
288+
output = str(site_root / "docs" / args.version / "docs-data.js")
289+
else:
290+
output = str(site_root / "docs" / "docs-data.js")
291+
292+
print("PHPER Documentation Converter")
232293
print(f" Source: {doc_dir}")
233294
print(f" Output: {output}")
295+
if args.version:
296+
print(f" Version: {args.version}")
234297
print()
235298

236299
sections = scan_doc_dir(doc_dir)
@@ -239,6 +302,11 @@ def main():
239302
sys.exit(1)
240303

241304
write_docs_data(sections, output)
305+
306+
# Keep versions.js in sync when a version was supplied
307+
if args.version:
308+
update_versions_js(args.version, site_root)
309+
242310
print("\nDone! Open docs.html to view the documentation.")
243311

244312

0 commit comments

Comments
 (0)