Source browser
scripts/highlight_docs.py
Read the complete file below or download its original bytes. Line numbers provide stable links to a declaration or processing step.
File fingerprint
1#!/usr/bin/env python3
2"""Render static syntax highlighting with exact source text.
3
4Source generators call highlight_code or highlight_lines before adding line
5anchors. The command-line pass handles guide code excerpts and preserves logs,
6tables and numbered sources. Pygments renders token spans during HTML creation.
7Browsers display the resulting markup with the local stylesheet.
8"""
9import argparse # Select HTML pages and an optional verification-only pass.
10from html import escape, unescape # Preserve code text while making token markup safe.
11from html.parser import HTMLParser # Locate the relevant code element boundaries.
12import os # Calculate a local stylesheet link for nested documentation pages.
13from pathlib import Path # Resolve source languages and static documentation files.
14import re # Recognize explicit language markers and conservative unlabeled examples.
15
16
17ALIASES = { # Accept the language names already used by the maintained guides.
18 "cpp": "cpp", "c++": "cpp", "cxx": "cpp", "cc": "cpp", "c": "cpp", # Use the established C++ lexer for the shared header and host examples.
19 "glsl": "glsl", "vert": "glsl", "frag": "glsl", "comp": "glsl", # Use native shader syntax for shader source bodies.
20 "py": "python", "python3": "python", "python": "python", # Keep Python examples independent of interpreter version spelling.
21 "sh": "bash", "shell": "bash", "console": "bash", "bash": "bash", # Read documented command lines as shell input.
22 "cmake": "cmake", "js": "javascript", "javascript": "javascript", # Support maintained build integration and browser helpers.
23 "css": "css", "html": "html", "markdown": "markdown", "md": "markdown", # Support the source browser's remaining text formats.
24 "text": "text", "plaintext": "text", "plain": "text", "none": "text", # Leave deliberate plain-text examples uncolored.
25} # Keep unknown language names on a safe plain-text fallback.
26
27
28def language_for_path(path): # Choose the source lexer from its path and language label.
29 """Return a stable lexer name for a repository-relative source path."""
30 path = Path(path) # Accept both strings and Path objects from documentation generators.
31 parts = path.as_posix().lower() # Compare directory names consistently on all development hosts.
32 if "/examples/shaders/" in "/" + parts or path.suffix.lower() in (".comp", ".vert", ".frag", ".glsl"): # Use GLSL syntax for native shader .cpp files.
33 return "glsl" # Preserve native qualifiers, buffer declarations and builtin names.
34 if path.name == "CMakeLists.txt" or path.suffix.lower() == ".cmake": # Recognize both top-level and reusable CMake files.
35 return "cmake" # Use the established CMake command and variable rules.
36 extensions = {".h": "cpp", ".hpp": "cpp", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".py": "python", ".sh": "bash", ".bash": "bash", ".js": "javascript", ".css": "css", ".html": "html", ".md": "markdown"} # Cover every format selected by the source browser.
37 return extensions.get(path.suffix.lower(), "text") # Render other files as plain text.
38
39
40def _pygments(): # Load the optional maintainer dependency only when highlighting is requested.
41 try: # Keep normal library builds and HTML reading independent of this package.
42 from pygments.lexers import get_lexer_by_name # Use the language lexer supplied by Pygments.
43 from pygments.token import STANDARD_TYPES # Reuse stable Pygments token category abbreviations.
44 except ImportError as failure: # Report exactly which optional documentation tool is missing.
45 raise RuntimeError("Documentation highlighting requires Pygments. Install it with: python3 -m pip install Pygments. Reading the HTML and building shdr do not require it.") from failure # Avoid a fallback that silently removes requested highlighting.
46 return get_lexer_by_name, STANDARD_TYPES # Keep the public helper import itself free of optional dependencies.
47
48
49def _escape_text(text): # Preserve carriage returns even when a browser normalizes HTML source newlines.
50 return escape(text, quote=False).replace("\r", " ") # Preserve quote characters as source text.
51
52
53def highlight_code(text, language="cpp"): # Highlight a complete source before any numbered line wrapping.
54 """Return escaped inline token spans with exact text.
55
56 Full-input lexing preserves multiline comment and string state. Each line
57 contains balanced spans and retains its original newline boundary. Callers
58 add code wrappers, line numbers and navigation links around this markup.
59 """
60 lexer_factory, standard = _pygments() # Give every caller the same optional dependency check.
61 language = ALIASES.get(language.lower(), "text") # Keep explicit plain text safe and predictable.
62 lexer = lexer_factory(language, stripnl=False, stripall=False, ensurenl=False, tabsize=0) # Disable whitespace trimming, added newlines and tab expansion.
63 tokens = list(lexer.get_tokens_unprocessed(text)) # Avoid the convenience API's newline and byte-order-mark preprocessing.
64 if "".join(value for _, _, value in tokens) != text: # Refuse any lexer result that would change the displayed source.
65 raise ValueError("Pygments changed source text while highlighting " + language) # Keep exact source preservation an enforced contract.
66 pieces = [] # Accumulate safe inline markup in original token order.
67 for _, token, value in tokens: # Reuse full-file lexer state across line boundaries.
68 parent = token # Find the nearest token category supported by the local stylesheet.
69 while parent not in standard and parent.parent is not None: # Handle lexer-specific token subcategories using established parent categories.
70 parent = parent.parent # Keep classification stable across Pygments versions.
71 category = standard.get(parent, "") # Append ordinary text directly.
72 fragments = value.split("\n") # Close token spans before every source line boundary.
73 for index, fragment in enumerate(fragments): # Reopen multiline token categories independently on later lines.
74 if index: # Emit exactly the newline present in the original token.
75 pieces.append("\n") # Keep line anchors aligned with the unchanged source text.
76 if fragment: # Select nonempty text fragments for token spans.
77 encoded = _escape_text(fragment) # Keep source markup and ampersands inert.
78 pieces.append('<span class="tok-' + category + '">' + encoded + '</span>' if category else encoded) # Apply local token style classes.
79 return "".join(pieces) # Return inline markup ready for existing source and signature wrappers.
80
81
82def highlight_lines(text, language="cpp"): # Share exact full-file lexing with numbered source generators.
83 """Return balanced inline HTML lines, including a final empty line when present."""
84 return highlight_code(text, language).split("\n") # Let existing generators retain their own line-count and anchor conventions.
85
86
87class _Stylesheets(HTMLParser): # Read existing stylesheet links and their source positions.
88 def __init__(self): # Start with an empty set of declared stylesheet URLs.
89 super().__init__(convert_charrefs=True) # Decode HTML entities in existing href attributes.
90 self.hrefs = set() # Retain only links relevant to idempotent insertion.
91
92 def handle_starttag(self, tag, attrs): # Read the original link metadata.
93 attrs = dict(attrs) # Look up attributes independently of their ordering.
94 if tag == "link" and "stylesheet" in attrs.get("rel", "").split(): # Ignore icons and unrelated resource links.
95 self.hrefs.add(attrs.get("href", "")) # Keep the exact already declared stylesheet target.
96
97
98def add_stylesheet(document, href="highlight.css"): # Add a local stylesheet while preserving page content and anchors.
99 """Insert the given stylesheet before head closes, unless it is already linked."""
100 existing = _Stylesheets() # Preserve every unrelated byte in the original HTML.
101 existing.feed(document) # Inspect only existing link elements.
102 if href in existing.hrefs: # Reuse the existing generator-provided stylesheet.
103 return document # Keep repeated generation byte-idempotent.
104 closing = re.search(r"</head\s*>", document, re.IGNORECASE) # Find a complete document's stylesheet insertion point.
105 if not closing: # Avoid inserting presentation markup into a fragment or compiler log.
106 raise ValueError("A complete HTML head is required to add highlighting CSS") # Explain the caller's missing document shell.
107 link = '<link rel="stylesheet" href="' + escape(href, quote=True) + '">' # Keep the generated URL local and safely quoted.
108 return document[:closing.start()] + link + document[closing.start():] # Preserve all existing IDs, links and formatting.
109
110
111class _CodeBlocks(HTMLParser): # Locate eligible guide code while preserving unrelated HTML byte-for-byte.
112 def __init__(self, document): # Build offset mapping for the original document.
113 super().__init__(convert_charrefs=False) # Keep original entity spellings outside selected code interiors.
114 self.document, self.blocks, self.pre, self.code = document, [], [], None # Track only candidate pre/code sections.
115 self.offsets = [0] # Map HTMLParser line positions back to original string offsets.
116 for line in document.splitlines(keepends=True): # Preserve newline lengths while building the map.
117 self.offsets.append(self.offsets[-1] + len(line)) # Retain the start offset for every parser line.
118
119 def position(self): # Convert the parser's current token position to an absolute offset.
120 line, column = self.getpos() # Read the beginning of the current HTML tag.
121 return self.offsets[line - 1] + column # Keep edits confined to the selected code interior.
122
123 def handle_starttag(self, tag, attrs): # Find code blocks and record any nested markup that must be preserved.
124 attrs = dict(attrs) # Read language and source-browser markers by name.
125 if tag == "pre": # Remember whether an existing numbered source wrapper owns this content.
126 self.pre.append(attrs) # Keep source blocks out of the guide postprocessor.
127 if self.code is not None: # Preserve existing anchors, links and syntax spans.
128 self.code["nested"] = True # Let the original generator own any structured code block.
129 if tag == "code" and self.pre and self.code is None: # Select explicit pre/code source excerpts.
130 if "source-code" in self.pre[-1].get("class", "").split(): # Numbered files are highlighted before line wrapping by their generator.
131 return # Preserve structured source lines and encoded downloads.
132 start = self.position() + len(self.get_starttag_text()) # Begin immediately after the unchanged code opening tag.
133 self.code = {"start": start, "attrs": attrs, "nested": False} # Store only the information needed for a safe interior replacement.
134
135 def handle_endtag(self, tag): # Close this candidate block at its original page boundary.
136 if tag == "code" and self.code is not None: # Capture the original unmodified interior boundaries.
137 self.code["end"] = self.position() # Stop before the original closing tag.
138 self.blocks.append(self.code) # Defer replacement until all original offsets are known.
139 self.code = None # Wait for the next explicit pre/code excerpt.
140 if tag == "pre" and self.pre: # Leave the current code or log wrapper.
141 self.pre.pop() # Preserve nesting information for later blocks.
142
143
144def _block_language(attrs, text): # Prefer explicit guide markers over conservative content recognition.
145 for value in attrs.get("class", "").split(): # Accept the language-* and lang-* conventions used by static guides.
146 if value.startswith(("language-", "lang-")): # Keep explicit plain text out of automatic coloring.
147 return ALIASES.get(value.split("-", 1)[1].lower(), "text") # Keep the existing markup when its convention needs a custom parser.
148 if re.search(r"\blayout\s*\(|\bgl_(?:Global|Local)InvocationID\b", text) or re.search(r"^\s*#version\b", text, re.MULTILINE): # Recognize unlabeled native shader examples.
149 return "glsl" # Preserve shader qualifiers and builtins.
150 if re.search(r"^\s*(?:cmake|ctest|python3?|git|SHDR_\w+=|\./build/|pip\s)\b", text, re.MULTILINE): # Recognize the documented shell command prefixes.
151 return "bash" # Color the command text in its declared shell language.
152 if re.search(r"\b(?:cmake_minimum_required|add_executable|target_link_libraries|shdr_target_shaders)\s*\(", text): # Recognize unlabeled CMake integration excerpts.
153 return "cmake" # Use CMake variable and command rules.
154 if re.search(r"^\s*(?:from\s+\w+\s+import|import\s+\w+|def\s+\w+\s*\()",
155 text, re.MULTILINE): # Recognize Python source snippets.
156 return "python" # Keep imports and function definitions in their actual language.
157 if re.search(r"\b(?:shdr::|std::|template\s*<|void\s+main\s*\(|#include|float\s+\w+\s*[=(])", text): # Recognize the remaining documented C++ examples and signatures.
158 return "cpp" # Use the same lexer as the API declaration generator.
159 return "text" # Leave unclassified logs, diagrams and literal data unchanged.
160
161
162def highlight_document(document, stylesheet="highlight.css"): # Process guide excerpts and preserve structured sources and logs.
163 """Return idempotently highlighted HTML with unchanged visible code and links."""
164 reader = _CodeBlocks(document) # Keep original offsets while inspecting block structure.
165 reader.feed(document) # Locate only explicit pre/code excerpts.
166 for block in reversed(reader.blocks): # Preserve earlier offsets while replacing later code interiors.
167 if block["nested"]: # Existing spans and anchors belong to a generator or earlier highlighting pass.
168 continue # Avoid duplicate token spans and line-gutter corruption.
169 text = unescape(document[block["start"]:block["end"]]) # Recover the original visible code once before lexing.
170 language = _block_language(block["attrs"], text) # Respect explicit language markers and conservative fallback rules.
171 if language == "text": # Preserve raw logs and deliberate text examples byte-for-byte.
172 continue # Keep tables and diagram-like content out of syntax processing.
173 encoded = highlight_code(text, language) # Preserve full-block multiline state and all original code characters.
174 document = document[:block["start"]] + encoded + document[block["end"]:] # Retain the original code tags, IDs and surrounding prose.
175 return add_stylesheet(document, stylesheet) # Keep all highlighting local and independent of browser JavaScript.
176
177
178def main(): # Apply the reusable guide postprocessor to a static documentation tree.
179 root = Path(__file__).resolve().parent.parent # Resolve default paths independently of the current shell directory.
180 parser = argparse.ArgumentParser(description=__doc__) # Explain that this is optional maintainer tooling.
181 parser.add_argument("paths", nargs="*", type=Path) # Accept individual HTML pages or a complete documentation tree.
182 parser.add_argument("--docs", "--site", dest="docs", type=Path, default=root / "docs") # Locate the shared local highlighting stylesheet.
183 parser.add_argument("--check", action="store_true") # Report stale highlighting in check mode.
184 args = parser.parse_args() # Read explicit regeneration or verification choices.
185 files = sorted({file for path in (args.paths or [args.docs]) for file in (path.rglob("*.html") if path.is_dir() else [path]) if file.suffix.lower() == ".html"}) # Process only static HTML artifacts.
186 changed = [] # Track files whose excerpt markup or stylesheet needs updating.
187 try: # Turn a missing optional package into one useful maintainer diagnostic.
188 for path in files: # Keep scientific SVGs and all non-HTML data outside this pass.
189 before = path.read_text(encoding="utf-8") # Read exact original markup for idempotence comparison.
190 href = Path(os.path.relpath(args.docs / "highlight.css", path.parent)).as_posix() # Keep nested source and benchmark styles local.
191 after = highlight_document(before, href) # Preserve logs, record values, IDs, anchors and downloads.
192 if before != after: # Avoid rewriting pages whose markup is already current.
193 changed.append(path) # Include the page in the maintainer summary.
194 if not args.check: # Verification mode must remain read-only.
195 path.write_text(after, encoding="utf-8") # Write only the static presentation changes.
196 except (RuntimeError, ValueError) as failure: # Report missing dependencies or a preservation contract violation clearly.
197 parser.exit(2, str(failure) + "\n") # Avoid an opaque traceback for a routine documentation setup problem.
198 print(("Needs highlighting: " if args.check else "Updated highlighting: ") + str(len(changed)) + " of " + str(len(files)) + " HTML pages") # Summarize the updated page count.
199 if args.check and changed: # Let a maintainer or CI check detect stale presentation.
200 parser.exit(1) # Keep the command's status consistent with its verification result.
201
202
203if __name__ == "__main__": # Run the postprocessor only when invoked explicitly.
204 main() # Render or check the documentation's static syntax highlighting.