Check the doxygen version
[platform/upstream/libxkbcommon.git] / scripts / ensure-stable-doc-urls.py
1 #!/usr/bin/env python3
2
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.
6 #
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.
10
11 import argparse
12 from enum import IntFlag
13 import glob
14 from itertools import chain
15 from pathlib import Path
16 from string import Template
17 from typing import NamedTuple, Sequence
18
19 import yaml
20
21
22 class Update(NamedTuple):
23     new: str
24     old: str
25
26
27 class ExitCode(IntFlag):
28     NORMAL = 0
29     INVALID_UPDATES = 1 << 4
30     MISSING_UPDATES = 1 << 5
31
32
33 THIS_SCRIPT_PATH = Path(__file__)
34 RELATIVE_SCRIPT_PATH = THIS_SCRIPT_PATH.relative_to(THIS_SCRIPT_PATH.parent.parent)
35
36 REDIRECTION_DELAY = 6  # in seconds. Note: at least 6s for accessibility
37
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
40 #
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(
46     """<!DOCTYPE HTML>
47 <html lang="en-US">
48     <head>
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>
54     </head>
55     <body>
56         <div id="top">
57             <div id="titlearea" style="padding: 1em 0 1em 0.5em;">
58                 <div id="projectname">
59                     libxkbcommon
60                 </div>
61             </div>
62         </div>
63         <div>
64             <div class="header">
65                 <div class="headertitle">
66                     <div class="title">🔀 Redirection</div>
67                 </div>
68             </div>
69             <div class="contents">
70                 <p>This page has been moved.</p>
71                 <p>
72                     If you are not redirected automatically,
73                     follow the <a href="${canonical}">link to the current page</a>.
74                 </p>
75             </div>
76         </div>
77     </body>
78 </html>
79 """
80 )
81
82
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ʹ}")
87     return updateʹ
88
89
90 def update_registry(registry_path: Path, doc_dir: Path, updates: Sequence[str]):
91     """
92     Update the URL registry by:
93     • Adding new pages
94     • Updating page aliases
95     """
96     # Parse updates
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 {}
101     # Expected updates
102     missing_updates = set(file for file in registry if not (doc_dir / file).is_file())
103     # Update
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:
109             continue
110         # Get previous entry and potential update
111         old = updates_.get(file)
112         if old:
113             # Update old entry
114             invalid_updates.remove(file)
115             entry = registry.get(old)
116             if entry is None:
117                 raise ValueError(f"Invalid update: {file}<-{old}")
118             else:
119                 del registry[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}”")
123         else:
124             entry = registry.get(file)
125             if entry is None:
126                 # New entry
127                 registry[file] = []
128                 print(f"[INFO] Added: {file}")
129             else:
130                 # Keep previous entry
131                 pass
132     exit_code = ExitCode.NORMAL
133     # Check
134     if invalid_updates:
135         for update in invalid_updates:
136             print(f"[ERROR] Update not processed: {update}")
137         exit_code |= ExitCode.INVALID_UPDATES
138     if missing_updates:
139         for old in missing_updates:
140             print(f"[ERROR] “{old}” not found and has no update.")
141         exit_code |= ExitCode.MISSING_UPDATES
142     if exit_code:
143         print(f"[ERROR] Processing interrupted: please fix the errors above.")
144         exit(exit_code.value)
145     # Write changes
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")
149         yaml.dump(
150             registry,
151             fd,
152         )
153
154
155 def generate_redirections(registry_path: Path, doc_dir: Path):
156     """
157     Create redirection pages using the aliases in the given URL registry.
158     """
159     cool = True
160     # Load 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():
166             cool = False
167             print(
168                 f"ERROR: missing canonical documentation page “{canonical}”. "
169                 f"Please update “{registry_path}” using {RELATIVE_SCRIPT_PATH}”."
170             )
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:
175                 fd.write(
176                     REDIRECTION_PAGE_TEMPLATE.substitute(
177                         canonical=canonical, delay=REDIRECTION_DELAY
178                     )
179                 )
180     if not cool:
181         exit(1)
182
183
184 def add_registry_argument(parser):
185     parser.add_argument(
186         "registry",
187         type=Path,
188         help="Path to the doc URI registry.",
189     )
190
191
192 def add_docdir_argument(parser):
193     parser.add_argument(
194         "docdir",
195         type=Path,
196         metavar="DOC_DIR",
197         help="Path to the generated HTML documentation directory.",
198     )
199
200
201 if __name__ == "__main__":
202     parser = argparse.ArgumentParser(
203         description="Tool to ensure HTML documentation has stable URLs"
204     )
205     subparsers = parser.add_subparsers()
206
207     parser_registry = subparsers.add_parser(
208         "update-registry", help="Update the registry of URIs"
209     )
210     add_registry_argument(parser_registry)
211     add_docdir_argument(parser_registry)
212     parser_registry.add_argument(
213         "updates",
214         nargs="*",
215         type=str,
216         help="Update: new=previous entries",
217     )
218     parser_registry.set_defaults(
219         run=lambda args: update_registry(args.registry, args.docdir, args.updates)
220     )
221
222     parser_redirections = subparsers.add_parser(
223         "generate-redirections", help="Generate URIs redirections"
224     )
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)
229     )
230
231     args = parser.parse_args()
232     args.run(args)