| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- #!/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
- import glob
- from enum import IntFlag
- 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
- NON_UNIQUE_DIRECTIONS = 1 << 6
- 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
- REDIRECTION_TITLE = "xkbcommon: Page Redirection"
- OPTIONAL_ENTRY = "__optional__"
- # 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>${title}</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 is_page_redirection(path: Path):
- with path.open("rt", encoding="utf-8") as fd:
- for line in fd:
- if REDIRECTION_TITLE in line:
- return True
- return False
- 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))
- # Update
- invalid_updates = set(updates_)
- # Load previous registry
- with registry_path.open("rt", encoding="utf-8") as fd:
- registry: dict[str, list[str]] = yaml.safe_load(fd) or {}
- registryʹ = dict(
- (canonical, aliases)
- for canonical, aliases in registry.items()
- if canonical != OPTIONAL_ENTRY
- )
- # Expected updates
- missing_updates = set(
- canonical for canonical in registryʹ if not (doc_dir / canonical).is_file()
- )
- # Ensure each page is unique
- for d, rs in registryʹ.items():
- if clashes := frozenset(rs).intersection(registry):
- print(
- f"[ERROR] The following redirections of “{d}”",
- f"clash with canonical directions: {clashes}",
- )
- exit(ExitCode.NON_UNIQUE_DIRECTIONS)
- redirections = frozenset(chain.from_iterable(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
- if old := updates_.get(file):
- # 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 tuple(missing_updates):
- # Handle older Doxygen versions
- if old in registry.get(OPTIONAL_ENTRY, []):
- print(
- "[WARNING] Handling old Doxygen version:",
- f"skip optional “{old}”",
- )
- missing_updates.remove(old)
- continue
- old_redirections = registry[old]
- for r in old_redirections:
- path = doc_dir / r
- if path.is_file() and not is_page_redirection(path):
- print(
- "[WARNING] Handling old Doxygen version:",
- f"use “{r}” instead of “{old}” for the canonical direction",
- )
- missing_updates.remove(old)
- break
- else:
- print(f"[ERROR] “{old}” not found and has no update.")
- if missing_updates:
- exit_code |= ExitCode.MISSING_UPDATES
- if exit_code:
- print("[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("# 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: dict[str, list[str]] = yaml.safe_load(fd) or {}
- registryʹ = dict(
- (canonical, aliases)
- for canonical, aliases in registry.items()
- if canonical != OPTIONAL_ENTRY
- )
- for canonical, aliases in registryʹ.items():
- # Check canonical path is up-to-date
- if not (doc_dir / canonical).is_file():
- # Handle older Doxygen versions
- if canonical in registry.get(OPTIONAL_ENTRY, []):
- print(
- "[WARNING] Handling old Doxygen version:",
- f"skip optional “{canonical}”",
- )
- continue
- for r in aliases:
- path = doc_dir / r
- if path.is_file() and not is_page_redirection(path):
- print(
- "[WARNING] Handling old Doxygen version:",
- f"use “{r}” instead of “{canonical}” for the canonical direction",
- )
- canonical = r
- aliases.remove(r)
- break
- else:
- 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,
- title=REDIRECTION_TITLE,
- )
- )
- 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)
|