|
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('reference', 'Reference')} |
45 | | - ${link('examples', 'Examples')} |
46 | | - ${link('about', 'About')} |
47 | | - `; |
48 | | - } |
49 | | - |
50 | | - if (!document.getElementById('nav-shared-style')) { |
51 | | - const style = document.createElement('style'); |
52 | | - style.id = 'nav-shared-style'; |
53 | | - style.textContent = ` |
54 | | - #site-nav { |
55 | | - border-bottom: 1px solid #e0e0e0; |
56 | | - padding: 0 2rem; |
57 | | - display: flex; |
58 | | - align-items: center; |
59 | | - justify-content: space-between; |
60 | | - height: 60px; |
61 | | - position: sticky; |
62 | | - top: 0; |
63 | | - background: #fff; |
64 | | - z-index: 100; |
65 | | - } |
66 | | - .nav-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; } |
67 | | - .nav-logo img { width: 28px; height: 28px; } |
68 | | - .nav-title { display: flex; flex-direction: column; line-height: 1.15; } |
69 | | - .nav-title-top { font-size: 14px; font-weight: 700; color: #e8b400; } |
70 | | - .nav-title-bottom { font-size: 14px; font-weight: 700; color: #e8b400; } |
71 | | - #nav-errors-link { margin-left: auto; font-size: 14px; font-weight: 700; color: #e8b400; text-decoration: none; } |
72 | | - #nav-errors-link:hover { color: #c99700; } |
73 | | - .hamburger { background: none; border: none; cursor: pointer; font-size: 22px; padding: 4px 8px; display: none; margin-left: 0.5rem; } |
74 | | - @media (max-width: 768px) { .hamburger { display: block; } } |
75 | | - `; |
76 | | - document.head.appendChild(style); |
77 | | - } |
78 | | - |
79 | | - if (!document.getElementById('search-wrap')) { |
80 | | - const s = document.createElement('script'); |
81 | | - s.src = SITE + '/assets/search.js'; |
82 | | - document.head.appendChild(s); |
83 | | - } |
84 | | -})(); |
| 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() |
0 commit comments