233 lines
7.4 KiB
Python
Executable File
233 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
# Doc URLs may change with time because they depend on Doxygen machinery.
|
||
# This is unfortunate because it is good practice to keep valid URLs.
|
||
# See: “Cool URIs don’t change” at https://www.w3.org/Provider/Style/URI.html.
|
||
#
|
||
# There is no built-in solution in Doxygen that we are aware of.
|
||
# The solution proposed here is to maintain a registry of all URLs and manage
|
||
# legacy URLs as redirections to their canonical page.
|
||
|
||
import argparse
|
||
from enum import IntFlag
|
||
import glob
|
||
from itertools import chain
|
||
from pathlib import Path
|
||
from string import Template
|
||
from typing import NamedTuple, Sequence
|
||
|
||
import yaml
|
||
|
||
|
||
class Update(NamedTuple):
|
||
new: str
|
||
old: str
|
||
|
||
|
||
class ExitCode(IntFlag):
|
||
NORMAL = 0
|
||
INVALID_UPDATES = 1 << 4
|
||
MISSING_UPDATES = 1 << 5
|
||
|
||
|
||
THIS_SCRIPT_PATH = Path(__file__)
|
||
RELATIVE_SCRIPT_PATH = THIS_SCRIPT_PATH.relative_to(THIS_SCRIPT_PATH.parent.parent)
|
||
|
||
REDIRECTION_DELAY = 6 # in seconds. Note: at least 6s for accessibility
|
||
|
||
# NOTE: The redirection works with the HTML tag: <meta http-equiv="refresh">.
|
||
# See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv
|
||
#
|
||
# NOTE: This page is a simplified version of the Doxygen-generated ones.
|
||
# It does use the current stylesheets, but it may break if the theme is updated.
|
||
# Ideally, we would just let Doxygen generate them, but I (Wismill) could not
|
||
# find a way to do this with the redirection feature.
|
||
REDIRECTION_PAGE_TEMPLATE = Template(
|
||
"""<!DOCTYPE HTML>
|
||
<html lang="en-US">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="refresh" content="${delay}; url=${canonical}">
|
||
<link href="doxygen.css" rel="stylesheet" type="text/css">
|
||
<link href="doxygen-extra.css" rel="stylesheet" type="text/css">
|
||
<title>xkbcommon: Page Redirection</title>
|
||
</head>
|
||
<body>
|
||
<div id="top">
|
||
<div id="titlearea" style="padding: 1em 0 1em 0.5em;">
|
||
<div id="projectname">
|
||
libxkbcommon
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="header">
|
||
<div class="headertitle">
|
||
<div class="title">🔀 Redirection</div>
|
||
</div>
|
||
</div>
|
||
<div class="contents">
|
||
<p>This page has been moved.</p>
|
||
<p>
|
||
If you are not redirected automatically,
|
||
follow the <a href="${canonical}">link to the current page</a>.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
)
|
||
|
||
|
||
def parse_page_update(update: str) -> Update:
|
||
updateʹ = Update(*update.split("="))
|
||
if updateʹ.new == updateʹ.old:
|
||
raise ValueError(f"Invalid update: {updateʹ}")
|
||
return updateʹ
|
||
|
||
|
||
def update_registry(registry_path: Path, doc_dir: Path, updates: Sequence[str]):
|
||
"""
|
||
Update the URL registry by:
|
||
• Adding new pages
|
||
• Updating page aliases
|
||
"""
|
||
# Parse updates
|
||
updates_ = dict(map(parse_page_update, updates))
|
||
# Load previous registry
|
||
with registry_path.open("rt", encoding="utf-8") as fd:
|
||
registry = yaml.safe_load(fd) or {}
|
||
# Expected updates
|
||
missing_updates = set(file for file in registry if not (doc_dir / file).is_file())
|
||
# Update
|
||
invalid_updates = set(updates_)
|
||
redirections = frozenset(chain(*registry.values()))
|
||
for file in glob.iglob("**/*.html", root_dir=doc_dir, recursive=True):
|
||
# Skip redirection pages
|
||
if file in redirections:
|
||
continue
|
||
# Get previous entry and potential update
|
||
old = updates_.get(file)
|
||
if old:
|
||
# Update old entry
|
||
invalid_updates.remove(file)
|
||
entry = registry.get(old)
|
||
if entry is None:
|
||
raise ValueError(f"Invalid update: {file}<-{old}")
|
||
else:
|
||
del registry[old]
|
||
missing_updates.remove(old)
|
||
registry[file] = [e for e in [old] + entry if e != file]
|
||
print(f"[INFO] Updated: “{old}” to “{file}”")
|
||
else:
|
||
entry = registry.get(file)
|
||
if entry is None:
|
||
# New entry
|
||
registry[file] = []
|
||
print(f"[INFO] Added: {file}")
|
||
else:
|
||
# Keep previous entry
|
||
pass
|
||
exit_code = ExitCode.NORMAL
|
||
# Check
|
||
if invalid_updates:
|
||
for update in invalid_updates:
|
||
print(f"[ERROR] Update not processed: {update}")
|
||
exit_code |= ExitCode.INVALID_UPDATES
|
||
if missing_updates:
|
||
for old in missing_updates:
|
||
print(f"[ERROR] “{old}” not found and has no update.")
|
||
exit_code |= ExitCode.MISSING_UPDATES
|
||
if exit_code:
|
||
print(f"[ERROR] Processing interrupted: please fix the errors above.")
|
||
exit(exit_code.value)
|
||
# Write changes
|
||
with registry_path.open("wt", encoding="utf-8") as fd:
|
||
fd.write(f"# WARNING: This file is autogenerated by: {RELATIVE_SCRIPT_PATH}\n")
|
||
fd.write(f"# Do not edit manually.\n")
|
||
yaml.dump(
|
||
registry,
|
||
fd,
|
||
)
|
||
|
||
|
||
def generate_redirections(registry_path: Path, doc_dir: Path):
|
||
"""
|
||
Create redirection pages using the aliases in the given URL registry.
|
||
"""
|
||
cool = True
|
||
# Load registry
|
||
with registry_path.open("rt", encoding="utf-8") as fd:
|
||
registry = yaml.safe_load(fd) or {}
|
||
for canonical, aliases in registry.items():
|
||
# Check canonical path is up-to-date
|
||
if not (doc_dir / canonical).is_file():
|
||
cool = False
|
||
print(
|
||
f"ERROR: missing canonical documentation page “{canonical}”. "
|
||
f"Please update “{registry_path}” using {RELATIVE_SCRIPT_PATH}”."
|
||
)
|
||
# Add a redirection page
|
||
for alias in aliases:
|
||
path = doc_dir / alias
|
||
with path.open("wt", encoding="utf-8") as fd:
|
||
fd.write(
|
||
REDIRECTION_PAGE_TEMPLATE.substitute(
|
||
canonical=canonical, delay=REDIRECTION_DELAY
|
||
)
|
||
)
|
||
if not cool:
|
||
exit(1)
|
||
|
||
|
||
def add_registry_argument(parser):
|
||
parser.add_argument(
|
||
"registry",
|
||
type=Path,
|
||
help="Path to the doc URI registry.",
|
||
)
|
||
|
||
|
||
def add_docdir_argument(parser):
|
||
parser.add_argument(
|
||
"docdir",
|
||
type=Path,
|
||
metavar="DOC_DIR",
|
||
help="Path to the generated HTML documentation directory.",
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(
|
||
description="Tool to ensure HTML documentation has stable URLs"
|
||
)
|
||
subparsers = parser.add_subparsers()
|
||
|
||
parser_registry = subparsers.add_parser(
|
||
"update-registry", help="Update the registry of URIs"
|
||
)
|
||
add_registry_argument(parser_registry)
|
||
add_docdir_argument(parser_registry)
|
||
parser_registry.add_argument(
|
||
"updates",
|
||
nargs="*",
|
||
type=str,
|
||
help="Update: new=previous entries",
|
||
)
|
||
parser_registry.set_defaults(
|
||
run=lambda args: update_registry(args.registry, args.docdir, args.updates)
|
||
)
|
||
|
||
parser_redirections = subparsers.add_parser(
|
||
"generate-redirections", help="Generate URIs redirections"
|
||
)
|
||
add_registry_argument(parser_redirections)
|
||
add_docdir_argument(parser_redirections)
|
||
parser_redirections.set_defaults(
|
||
run=lambda args: generate_redirections(args.registry, args.docdir)
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
args.run(args)
|