|
| 1 | +# SPDX-FileCopyrightText: 2026 PyThaiNLP Project |
| 2 | +# SPDX-FileType: SOURCE |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | +""" |
| 5 | +Update documentation index pages on release. |
| 6 | +
|
| 7 | +This script performs two tasks: |
| 8 | +
|
| 9 | +1. Generates or updates ``index.html`` in the versioned docs repository |
| 10 | + (``PyThaiNLP/docs``) to list all released versions as a landing page. |
| 11 | +2. Injects or updates a released-versions section in ``index.html`` of the |
| 12 | + development docs repository (``PyThaiNLP/dev-docs``). |
| 13 | +
|
| 14 | +Usage:: |
| 15 | +
|
| 16 | + python update_docs_index.py \\ |
| 17 | + --docs-dir /path/to/docs \\ |
| 18 | + --dev-docs-index /path/to/dev-docs/index.html \\ |
| 19 | + --version 5.3.1 |
| 20 | +""" |
| 21 | + |
| 22 | +from __future__ import annotations |
| 23 | + |
| 24 | +import argparse |
| 25 | +import re |
| 26 | +import sys |
| 27 | +from pathlib import Path |
| 28 | + |
| 29 | +# Markers used to delimit the injected versions section in dev-docs/index.html. |
| 30 | +_VERSIONS_BEGIN: str = "<!-- BEGIN VERSIONS -->" |
| 31 | +_VERSIONS_END: str = "<!-- END VERSIONS -->" |
| 32 | + |
| 33 | +_VERSIONS_SECTION_TMPL: str = """\ |
| 34 | +<!-- BEGIN VERSIONS --> |
| 35 | +<section id="released-versions"> |
| 36 | + <h2>Released versions</h2> |
| 37 | + <ul> |
| 38 | +{items} |
| 39 | + </ul> |
| 40 | +</section> |
| 41 | +<!-- END VERSIONS -->""" |
| 42 | + |
| 43 | +_VERSION_ITEM_TMPL: str = ( |
| 44 | + ' <li><a href="https://pythainlp.github.io/docs/{v}/">{v}</a></li>' |
| 45 | +) |
| 46 | + |
| 47 | +_DOCS_INDEX_TMPL: str = """\ |
| 48 | +<!DOCTYPE html> |
| 49 | +<html lang="en"> |
| 50 | +<head> |
| 51 | + <meta charset="UTF-8"> |
| 52 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 53 | + <title>PyThaiNLP Documentation</title> |
| 54 | + <style> |
| 55 | + body {{ |
| 56 | + font-family: sans-serif; |
| 57 | + max-width: 800px; |
| 58 | + margin: 2rem auto; |
| 59 | + padding: 0 1rem; |
| 60 | + }} |
| 61 | + h1 {{ color: #333; }} |
| 62 | + ul {{ list-style: none; padding: 0; }} |
| 63 | + li {{ margin: 0.5rem 0; }} |
| 64 | + a {{ color: #0066cc; text-decoration: none; }} |
| 65 | + a:hover {{ text-decoration: underline; }} |
| 66 | + </style> |
| 67 | +</head> |
| 68 | +<body> |
| 69 | + <h1>PyThaiNLP Documentation</h1> |
| 70 | + <p> |
| 71 | + Latest development docs: |
| 72 | + <a href="https://pythainlp.github.io/dev-docs/">dev-docs</a> |
| 73 | + </p> |
| 74 | + <h2>Released versions</h2> |
| 75 | + <ul> |
| 76 | +{items} |
| 77 | + </ul> |
| 78 | +</body> |
| 79 | +</html> |
| 80 | +""" |
| 81 | + |
| 82 | + |
| 83 | +def _is_version_dir(name: str) -> bool: |
| 84 | + """Return True if *name* looks like a version directory (e.g. ``5.3.1``).""" |
| 85 | + return bool(re.match(r"^\d+\.\d+", name)) |
| 86 | + |
| 87 | + |
| 88 | +def _version_key(version: str) -> tuple: |
| 89 | + """Return a sort key for a version string.""" |
| 90 | + parts = re.split(r"[.\-]", version) |
| 91 | + key: list[int] = [] |
| 92 | + for part in parts: |
| 93 | + try: |
| 94 | + key.append(int(part)) |
| 95 | + except ValueError: |
| 96 | + break |
| 97 | + return tuple(key) |
| 98 | + |
| 99 | + |
| 100 | +def _get_version_dirs(docs_dir: Path) -> list[str]: |
| 101 | + """Return version directories in *docs_dir*, sorted newest-first.""" |
| 102 | + versions = [ |
| 103 | + p.name |
| 104 | + for p in docs_dir.iterdir() |
| 105 | + if p.is_dir() and _is_version_dir(p.name) |
| 106 | + ] |
| 107 | + versions.sort(key=_version_key, reverse=True) |
| 108 | + return versions |
| 109 | + |
| 110 | + |
| 111 | +def update_docs_index(docs_dir: Path) -> None: |
| 112 | + """Generate or overwrite ``docs_dir/index.html`` listing all versions. |
| 113 | +
|
| 114 | + :param docs_dir: Path to the versioned documentation repository root. |
| 115 | + """ |
| 116 | + versions = _get_version_dirs(docs_dir) |
| 117 | + items = "\n".join( |
| 118 | + f' <li><a href="{v}/">{v}</a></li>' for v in versions |
| 119 | + ) |
| 120 | + content = _DOCS_INDEX_TMPL.format(items=items) |
| 121 | + index_path = docs_dir / "index.html" |
| 122 | + index_path.write_text(content, encoding="utf-8") |
| 123 | + print(f"Updated {index_path}") |
| 124 | + |
| 125 | + |
| 126 | +def update_dev_docs_index(index_path: Path, version: str) -> None: |
| 127 | + """Inject or update a released-versions section in *index_path*. |
| 128 | +
|
| 129 | + Looks for ``<!-- BEGIN VERSIONS -->`` / ``<!-- END VERSIONS -->`` markers. |
| 130 | + If found, prepends *version* to the existing list (skips if already there). |
| 131 | + If not found, appends a new section before ``</body>`` (or at end of file). |
| 132 | +
|
| 133 | + :param index_path: Path to ``dev-docs/index.html``. |
| 134 | + :param version: Version string without leading ``v`` (e.g. ``5.3.1``). |
| 135 | + """ |
| 136 | + content = index_path.read_text(encoding="utf-8") |
| 137 | + new_item = _VERSION_ITEM_TMPL.format(v=version) |
| 138 | + |
| 139 | + if _VERSIONS_BEGIN in content and _VERSIONS_END in content: |
| 140 | + begin = content.index(_VERSIONS_BEGIN) |
| 141 | + end = content.index(_VERSIONS_END) + len(_VERSIONS_END) |
| 142 | + section = content[begin:end] |
| 143 | + |
| 144 | + # Skip if this version is already listed. |
| 145 | + if f"/{version}/" in section: |
| 146 | + print(f"{index_path}: version {version} already present, skipping.") |
| 147 | + return |
| 148 | + |
| 149 | + # Prepend the new item inside the existing <ul> or <ol>. |
| 150 | + section = re.sub( |
| 151 | + r"(<[uo]l[^>]*>)", |
| 152 | + rf"\1\n{new_item}", |
| 153 | + section, |
| 154 | + count=1, |
| 155 | + ) |
| 156 | + content = content[:begin] + section + content[end:] |
| 157 | + else: |
| 158 | + # No markers yet — append a new section. |
| 159 | + new_section = _VERSIONS_SECTION_TMPL.format(items=new_item) |
| 160 | + if "</body>" in content: |
| 161 | + content = content.replace("</body>", f"\n{new_section}\n</body>") |
| 162 | + else: |
| 163 | + content = content + f"\n{new_section}\n" |
| 164 | + |
| 165 | + index_path.write_text(content, encoding="utf-8") |
| 166 | + print(f"Updated {index_path}") |
| 167 | + |
| 168 | + |
| 169 | +def main() -> None: |
| 170 | + """Entry point.""" |
| 171 | + parser = argparse.ArgumentParser( |
| 172 | + description="Update PyThaiNLP documentation index pages on release.", |
| 173 | + ) |
| 174 | + parser.add_argument( |
| 175 | + "--docs-dir", |
| 176 | + type=Path, |
| 177 | + help=( |
| 178 | + "Path to the versioned docs repository. " |
| 179 | + "Generates or overwrites index.html listing all version directories." |
| 180 | + ), |
| 181 | + ) |
| 182 | + parser.add_argument( |
| 183 | + "--dev-docs-index", |
| 184 | + type=Path, |
| 185 | + help=( |
| 186 | + "Path to dev-docs/index.html. " |
| 187 | + "Injects or updates a released-versions section." |
| 188 | + ), |
| 189 | + ) |
| 190 | + parser.add_argument( |
| 191 | + "--version", |
| 192 | + required=True, |
| 193 | + help="Version string without leading 'v' (e.g. 5.3.1).", |
| 194 | + ) |
| 195 | + args = parser.parse_args() |
| 196 | + |
| 197 | + if not args.docs_dir and not args.dev_docs_index: |
| 198 | + parser.error("At least one of --docs-dir or --dev-docs-index is required.") |
| 199 | + |
| 200 | + if args.docs_dir: |
| 201 | + if not args.docs_dir.is_dir(): |
| 202 | + print( |
| 203 | + f"Error: --docs-dir '{args.docs_dir}' is not a directory.", |
| 204 | + file=sys.stderr, |
| 205 | + ) |
| 206 | + sys.exit(1) |
| 207 | + update_docs_index(args.docs_dir) |
| 208 | + |
| 209 | + if args.dev_docs_index: |
| 210 | + if not args.dev_docs_index.exists(): |
| 211 | + print( |
| 212 | + f"Warning: --dev-docs-index '{args.dev_docs_index}' does not exist, " |
| 213 | + "skipping.", |
| 214 | + file=sys.stderr, |
| 215 | + ) |
| 216 | + else: |
| 217 | + update_dev_docs_index(args.dev_docs_index, args.version) |
| 218 | + |
| 219 | + |
| 220 | +if __name__ == "__main__": |
| 221 | + main() |
0 commit comments