3 # Doc URLs may change with time because they depend on Doxygen machinery.
4 # This is unfortunate because it is good practice to keep valid URLs.
5 # See: “Cool URIs don’t change” at https://www.w3.org/Provider/Style/URI.html.
7 # There is no built-in solution in Doxygen that we are aware of.
8 # The solution proposed here is to maintain a registry of all URLs and manage
9 # legacy URLs as redirections to their canonical page.
12 from enum import IntFlag
14 from itertools import chain
15 from pathlib import Path
16 from string import Template
17 from typing import NamedTuple, Sequence
22 class Update(NamedTuple):
27 class ExitCode(IntFlag):
29 INVALID_UPDATES = 1 << 4
30 MISSING_UPDATES = 1 << 5
33 THIS_SCRIPT_PATH = Path(__file__)
34 RELATIVE_SCRIPT_PATH = THIS_SCRIPT_PATH.relative_to(THIS_SCRIPT_PATH.parent.parent)
36 REDIRECTION_DELAY = 6 # in seconds. Note: at least 6s for accessibility
38 # NOTE: The redirection works with the HTML tag: <meta http-equiv="refresh">.
39 # See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#http-equiv
41 # NOTE: This page is a simplified version of the Doxygen-generated ones.
42 # It does use the current stylesheets, but it may break if the theme is updated.
43 # Ideally, we would just let Doxygen generate them, but I (Wismill) could not
44 # find a way to do this with the redirection feature.
45 REDIRECTION_PAGE_TEMPLATE = Template(
49 <meta charset="UTF-8">
50 <meta http-equiv="refresh" content="${delay}; url=${canonical}">
51 <link href="doxygen.css" rel="stylesheet" type="text/css">
52 <link href="doxygen-extra.css" rel="stylesheet" type="text/css">
53 <title>xkbcommon: Page Redirection</title>
57 <div id="titlearea" style="padding: 1em 0 1em 0.5em;">
58 <div id="projectname">
65 <div class="headertitle">
66 <div class="title">🔀 Redirection</div>
69 <div class="contents">
70 <p>This page has been moved.</p>
72 If you are not redirected automatically,
73 follow the <a href="${canonical}">link to the current page</a>.
83 def parse_page_update(update: str) -> Update:
84 updateʹ = Update(*update.split("="))
85 if updateʹ.new == updateʹ.old:
86 raise ValueError(f"Invalid update: {updateʹ}")
90 def update_registry(registry_path: Path, doc_dir: Path, updates: Sequence[str]):
92 Update the URL registry by:
94 • Updating page aliases
97 updates_ = dict(map(parse_page_update, updates))
98 # Load previous registry
99 with registry_path.open("rt", encoding="utf-8") as fd:
100 registry = yaml.safe_load(fd) or {}
102 missing_updates = set(file for file in registry if not (doc_dir / file).is_file())
104 invalid_updates = set(updates_)
105 redirections = frozenset(chain(*registry.values()))
106 for file in glob.iglob("**/*.html", root_dir=doc_dir, recursive=True):
107 # Skip redirection pages
108 if file in redirections:
110 # Get previous entry and potential update
111 old = updates_.get(file)
114 invalid_updates.remove(file)
115 entry = registry.get(old)
117 raise ValueError(f"Invalid update: {file}<-{old}")
120 missing_updates.remove(old)
121 registry[file] = [e for e in [old] + entry if e != file]
122 print(f"[INFO] Updated: “{old}” to “{file}”")
124 entry = registry.get(file)
128 print(f"[INFO] Added: {file}")
130 # Keep previous entry
132 exit_code = ExitCode.NORMAL
135 for update in invalid_updates:
136 print(f"[ERROR] Update not processed: {update}")
137 exit_code |= ExitCode.INVALID_UPDATES
139 for old in missing_updates:
140 print(f"[ERROR] “{old}” not found and has no update.")
141 exit_code |= ExitCode.MISSING_UPDATES
143 print(f"[ERROR] Processing interrupted: please fix the errors above.")
144 exit(exit_code.value)
146 with registry_path.open("wt", encoding="utf-8") as fd:
147 fd.write(f"# WARNING: This file is autogenerated by: {RELATIVE_SCRIPT_PATH}\n")
148 fd.write(f"# Do not edit manually.\n")
155 def generate_redirections(registry_path: Path, doc_dir: Path):
157 Create redirection pages using the aliases in the given URL registry.
161 with registry_path.open("rt", encoding="utf-8") as fd:
162 registry = yaml.safe_load(fd) or {}
163 for canonical, aliases in registry.items():
164 # Check canonical path is up-to-date
165 if not (doc_dir / canonical).is_file():
168 f"ERROR: missing canonical documentation page “{canonical}”. "
169 f"Please update “{registry_path}” using {RELATIVE_SCRIPT_PATH}”."
171 # Add a redirection page
172 for alias in aliases:
173 path = doc_dir / alias
174 with path.open("wt", encoding="utf-8") as fd:
176 REDIRECTION_PAGE_TEMPLATE.substitute(
177 canonical=canonical, delay=REDIRECTION_DELAY
184 def add_registry_argument(parser):
188 help="Path to the doc URI registry.",
192 def add_docdir_argument(parser):
197 help="Path to the generated HTML documentation directory.",
201 if __name__ == "__main__":
202 parser = argparse.ArgumentParser(
203 description="Tool to ensure HTML documentation has stable URLs"
205 subparsers = parser.add_subparsers()
207 parser_registry = subparsers.add_parser(
208 "update-registry", help="Update the registry of URIs"
210 add_registry_argument(parser_registry)
211 add_docdir_argument(parser_registry)
212 parser_registry.add_argument(
216 help="Update: new=previous entries",
218 parser_registry.set_defaults(
219 run=lambda args: update_registry(args.registry, args.docdir, args.updates)
222 parser_redirections = subparsers.add_parser(
223 "generate-redirections", help="Generate URIs redirections"
225 add_registry_argument(parser_redirections)
226 add_docdir_argument(parser_redirections)
227 parser_redirections.set_defaults(
228 run=lambda args: generate_redirections(args.registry, args.docdir)
231 args = parser.parse_args()