|
1 | | -""" |
2 | | -Retries thumbnail generation for any example that failed or was skipped |
3 | | -in the last run of generate_example_thumbnails.py. |
4 | | - |
5 | | -An example is considered "needs retry" if: |
6 | | - - It exists in examples_js/ but has no entry in manifest.json at all |
7 | | - (crashed before it could be recorded, or was never attempted) |
8 | | - - Its manifest entry has placeholder=True with reason="non_js_source" |
9 | | - is NOT retried -- that's a real content bug, not a transient failure |
10 | | - - Its manifest entry has placeholder=True without a reason IS retried |
11 | | - if --retry-placeholders flag is passed (off by default, since most |
12 | | - placeholders are intentional skips for data-loading sketches) |
13 | | - |
14 | | -Usage: |
15 | | - python3 retry_thumbnails.py |
16 | | - python3 retry_thumbnails.py --retry-placeholders |
17 | | -""" |
18 | | - |
19 | | -import os |
20 | | -import sys |
21 | | -import json |
22 | | -import argparse |
23 | | - |
24 | | -# ── reuse everything from the main script ────────────────────────────────── |
25 | | -sys.path.insert(0, os.path.dirname(__file__)) |
26 | | -import generate_example_thumbnails as gen |
27 | | -from playwright.sync_api import sync_playwright |
28 | | - |
29 | | -MANIFEST_PATH = os.path.join(gen.OUT_DIR, "manifest.json") |
30 | | - |
31 | | - |
32 | | -def load_manifest(): |
33 | | - if not os.path.exists(MANIFEST_PATH): |
34 | | - print("No manifest found -- run generate_example_thumbnails.py first.") |
35 | | - return {} |
36 | | - with open(MANIFEST_PATH) as f: |
37 | | - entries = json.load(f) |
38 | | - # Key on (section, slug) matching the new multi-section format |
39 | | - return { |
40 | | - (e.get("section", "Basics"), e["slug"]): e |
41 | | - for e in entries |
42 | | - } |
43 | | - |
44 | | - |
45 | | -def main(): |
46 | | - parser = argparse.ArgumentParser() |
47 | | - parser.add_argument( |
48 | | - "--retry-placeholders", |
49 | | - action="store_true", |
50 | | - help="Also retry intentional placeholder entries (sketches that load " |
51 | | - "external assets). Off by default since most are genuine skips.", |
52 | | - ) |
53 | | - args = parser.parse_args() |
54 | | - |
55 | | - manifest = load_manifest() |
56 | | - all_examples = gen.discover_examples() |
57 | | - |
58 | | - to_retry = [] |
59 | | - for ex in all_examples: |
60 | | - key = (ex["section"], ex["slug"]) |
61 | | - entry = manifest.get(key) |
62 | | - |
63 | | - if entry is None: |
64 | | - # Never made it into the manifest at all -- definite failure |
65 | | - to_retry.append((ex, "not in manifest")) |
66 | | - continue |
67 | | - |
68 | | - if entry.get("placeholder"): |
69 | | - reason = entry.get("reason", "") |
70 | | - if reason == "non_js_source": |
71 | | - # Real content bug -- skip, can't fix by retrying |
72 | | - continue |
73 | | - if args.retry_placeholders: |
74 | | - to_retry.append((ex, "placeholder retry requested")) |
75 | | - |
76 | | - if not to_retry: |
77 | | - print("Nothing to retry. All examples are in the manifest.") |
78 | | - return |
79 | | - |
80 | | - print(f"Retrying {len(to_retry)} example(s):") |
81 | | - for ex, reason in to_retry: |
82 | | - print(f" {ex['section']}/{ex['category']}/{ex['slug']} ({reason})") |
83 | | - print() |
84 | | - |
85 | | - # Load the full manifest to update it in place |
86 | | - all_entries = list(manifest.values()) |
87 | | - all_keys = set(manifest.keys()) |
88 | | - |
89 | | - asset_base_url, asset_httpd = gen.start_local_asset_server(gen.ASSETS_DIR) |
90 | | - print(f"Serving assets locally at {asset_base_url}") |
91 | | - |
92 | | - succeeded = 0 |
93 | | - still_failed = [] |
94 | | - |
95 | | - try: |
96 | | - with sync_playwright() as p: |
97 | | - browser = p.chromium.launch() |
98 | | - |
99 | | - for ex, _ in to_retry: |
100 | | - cat_out_dir = os.path.join(gen.OUT_DIR, ex["section"], ex["category"]) |
101 | | - os.makedirs(cat_out_dir, exist_ok=True) |
102 | | - out_path = os.path.join(cat_out_dir, ex["slug"] + ".png") |
103 | | - |
104 | | - with open(ex["js_path"], errors="replace") as f: |
105 | | - js_code = gen.strip_comment(f.read()) |
106 | | - |
107 | | - if gen._non_js_source_re.search(js_code): |
108 | | - print(f" SKIP (unconverted source): {ex['section']}/{ex['slug']}") |
109 | | - continue |
110 | | - |
111 | | - if gen._data_loading_re.search(js_code): |
112 | | - svg = gen.make_placeholder_svg(ex["name"], gen.THUMB_SIZE) |
113 | | - with open(out_path.replace(".png", ".svg"), "w") as f: |
114 | | - f.write(svg) |
115 | | - new_entry = {**ex, "thumb": ex["slug"] + ".svg", "placeholder": True} |
116 | | - _upsert(all_entries, all_keys, ex, new_entry) |
117 | | - print(f" placeholder: {ex['section']}/{ex['slug']}") |
118 | | - continue |
119 | | - |
120 | | - is_no_canvas = bool(gen._no_canvas_re.search(js_code)) |
121 | | - |
122 | | - try: |
123 | | - ok = gen.render_thumbnail( |
124 | | - browser, js_code, out_path, asset_base_url, |
125 | | - debug_label=f"{ex['section']}/{ex['category']}/{ex['slug']}", |
126 | | - is_no_canvas=is_no_canvas, |
127 | | - ) |
128 | | - if ok: |
129 | | - new_entry = {**ex, "thumb": ex["slug"] + ".png", "placeholder": False} |
130 | | - _upsert(all_entries, all_keys, ex, new_entry) |
131 | | - succeeded += 1 |
132 | | - print(f" rendered: {ex['section']}/{ex['slug']}") |
133 | | - else: |
134 | | - still_failed.append(ex["slug"]) |
135 | | - print(f" FAILED: {ex['section']}/{ex['slug']}") |
136 | | - except Exception as e: |
137 | | - still_failed.append(ex["slug"]) |
138 | | - print(f" FAILED ({e}): {ex['section']}/{ex['slug']}") |
139 | | - |
140 | | - browser.close() |
141 | | - finally: |
142 | | - asset_httpd.shutdown() |
143 | | - |
144 | | - with open(MANIFEST_PATH, "w") as f: |
145 | | - json.dump(all_entries, f, indent=2) |
146 | | - |
147 | | - print() |
148 | | - print(f"Succeeded: {succeeded}") |
149 | | - print(f"Still failed: {len(still_failed)}") |
150 | | - for slug in still_failed: |
151 | | - print(f" - {slug}") |
152 | | - print(f"Manifest updated: {MANIFEST_PATH}") |
153 | | - |
154 | | - |
155 | | -def _upsert(all_entries, all_keys, ex, new_entry): |
156 | | - """Replace an existing manifest entry in-place, or append if new.""" |
157 | | - key = (ex.get("section", "Basics"), ex["slug"]) |
158 | | - for i, e in enumerate(all_entries): |
159 | | - if (e.get("section", "Basics"), e["slug"]) == key: |
160 | | - all_entries[i] = new_entry |
161 | | - return |
162 | | - all_entries.append(new_entry) |
163 | | - all_keys.add(key) |
164 | | - |
165 | | - |
166 | | -if __name__ == "__main__": |
167 | | - main() |
| 1 | +(function() { |
| 2 | + const SITE = 'https://processing-cpp.github.io'; |
| 3 | + const path = window.location.pathname; |
| 4 | + const parts = path.replace(/\/$/, '').split('/').filter(Boolean); |
| 5 | + const isRoot = parts.length === 0 || (parts.length === 1 && parts[0] === 'index.html'); |
| 6 | + // Depth = number of directory levels below site root. A trailing |
| 7 | + // "index.html" or "<file>.html" segment doesn't count as a directory level. |
| 8 | + const hasTrailingFile = parts.length > 0 && parts[parts.length - 1].endsWith('.html'); |
| 9 | + const depth = isRoot ? 0 : (hasTrailingFile ? parts.length - 1 : parts.length); |
| 10 | + const prefix = depth === 0 ? '/' : '../'.repeat(depth); |
| 11 | + |
| 12 | + function isActive(name) { return path.includes('/' + name); } |
| 13 | + |
| 14 | + function link(href, label, style, activeKey) { |
| 15 | + const active = isActive(activeKey || href) ? ' class="active"' : ''; |
| 16 | + const s = style ? ` style="${style}"` : ''; |
| 17 | + return `<a href="${prefix}${href}"${active}${s}>${label}</a>`; |
| 18 | + } |
| 19 | + |
| 20 | + const nav = document.getElementById('site-nav'); |
| 21 | + if (nav) { |
| 22 | + nav.innerHTML = ` |
| 23 | + <a href="${SITE}" class="nav-logo"> |
| 24 | + <img src="${prefix}assets/cpp-logo.png" alt="Processing for C++"> |
| 25 | + <div class="nav-title"> |
| 26 | + <span class="nav-title-top">Processing</span> |
| 27 | + <span class="nav-title-bottom">C++</span> |
| 28 | + </div> |
| 29 | + </a> |
| 30 | + <button class="hamburger" onclick=" |
| 31 | + var s = document.querySelector('.sidebar-outer, .sidebar'); |
| 32 | + if(s) s.classList.toggle('open'); |
| 33 | + ">☰</button> |
| 34 | + <a href="${prefix}error/index.html" id="nav-errors-link"${isActive('error') ? ' class="active"' : ''}>Errors</a> |
| 35 | + `; |
| 36 | + } |
| 37 | + |
| 38 | + const sidebar = document.getElementById('site-sidebar') || document.querySelector('.sidebar'); |
| 39 | + if (sidebar) { |
| 40 | + sidebar.innerHTML = ` |
| 41 | + ${link('whats-new', "What's New", 'color:#e8b400;font-weight:700;')} |
| 42 | + <div style="height:1px;background:#e0e0e0;margin:0.5rem 0;"></div> |
| 43 | + ${link('libraries', 'Libraries')} |
| 44 | + ${link('downloads', 'Downloads')} |
| 45 | + ${link('tutorials', 'Tutorials')} |
| 46 | + ${link('reference', 'Reference')} |
| 47 | + ${link('examples', 'Examples')} |
| 48 | + ${link('about', 'About')} |
| 49 | + `; |
| 50 | + } |
| 51 | + |
| 52 | + if (!document.getElementById('nav-shared-style')) { |
| 53 | + const style = document.createElement('style'); |
| 54 | + style.id = 'nav-shared-style'; |
| 55 | + style.textContent = ` |
| 56 | + #site-nav { |
| 57 | + border-bottom: 1px solid #e0e0e0; |
| 58 | + padding: 0 2rem; |
| 59 | + display: flex; |
| 60 | + align-items: center; |
| 61 | + justify-content: space-between; |
| 62 | + height: 60px; |
| 63 | + position: sticky; |
| 64 | + top: 0; |
| 65 | + background: #fff; |
| 66 | + z-index: 100; |
| 67 | + } |
| 68 | + .nav-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; } |
| 69 | + .nav-logo img { width: 28px; height: 28px; } |
| 70 | + .nav-title { display: flex; flex-direction: column; line-height: 1.15; } |
| 71 | + .nav-title-top { font-size: 14px; font-weight: 700; color: #e8b400; } |
| 72 | + .nav-title-bottom { font-size: 14px; font-weight: 700; color: #e8b400; } |
| 73 | + #nav-errors-link { margin-left: auto; font-size: 14px; font-weight: 700; color: #e8b400; text-decoration: none; } |
| 74 | + #nav-errors-link:hover { color: #c99700; } |
| 75 | + .hamburger { background: none; border: none; cursor: pointer; font-size: 22px; padding: 4px 8px; display: none; margin-left: 0.5rem; } |
| 76 | + @media (max-width: 768px) { .hamburger { display: block; } } |
| 77 | + `; |
| 78 | + document.head.appendChild(style); |
| 79 | + } |
| 80 | + |
| 81 | + if (!document.getElementById('search-wrap')) { |
| 82 | + const s = document.createElement('script'); |
| 83 | + s.src = SITE + '/assets/search.js'; |
| 84 | + document.head.appendChild(s); |
| 85 | + } |
| 86 | +})(); |
0 commit comments