"""Writes examples/gallery/stories.json: every gallery page and its sections, in the order they draw. A section is a story the web gallery shows alone at ?page=&story=. Its components are the names in its title that are entries in tasks/*.md, so "Sizes" names none. `--check` fails if the committed file differs. """ import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PAGES = ROOT / "examples/gallery/pages" OUT = ROOT / "examples/gallery/stories.json" def strip(text): """Blanks comments and literals' insides, keeping offsets, so braces and calls parse.""" out, i, n = list(text), 0, len(text) def blank(a, b): for k in range(a, b): if out[k] != "\n": out[k] = " " while i < n: if text.startswith("//", i): j = text.find("\n", i) j = n if j < 0 else j blank(i, j) i = j elif text.startswith("/*", i): j = text.index("*/", i) + 2 blank(i, j) i = j elif m := re.match(r'b?r(#*)"', text[i:]): end = '"' + m.group(1) j = text.index(end, i + m.end()) + len(end) blank(i + m.end(), j - len(end)) i = j elif text[i] == '"': j = i + 1 while text[j] != '"': j += 2 if text[j] == "\\" else 1 blank(i + 1, j) i = j + 1 elif m := re.match(r"'(\\.[^']*|[^'\\])'", text[i:]): blank(i, i + m.end()) i += m.end() else: i += 1 return "".join(out) def literal(text, at): """The string literal starting at `at`, decoded.""" m = re.compile(r'"((?:[^"\\]|\\.)*)"', re.S).match(text, at) return json.loads('"' + re.sub(r"\\\n\s*", "", m.group(1)) + '"') def functions(folder): """Every `fn` in a page's folder: (module, name) -> (source, stripped, body start, body end).""" found = {} for file in sorted(folder.rglob("*.rs")): module = file.stem text = file.read_text() bare = strip(text) for m in re.finditer(r"\bfn\s+(\w+)\b", bare): depth, j = 0, bare.index("{", m.end()) if ";" in bare[m.end():j]: continue k = j while True: depth += {"{": 1, "}": -1}.get(bare[k], 0) if depth == 0: break k += 1 found[(module, m.group(1))] = (text, bare, j, k) return found def sections(folder): """Section titles in the order the page's render reaches them; fails on any it never reaches.""" fns = functions(folder) order, seen = [], set() def walk(module, name): if (module, name) in seen: return seen.add((module, name)) text, bare, start, end = fns[(module, name)] call = re.compile(r"(? 1}: sys.exit(f"{module}: two stories answer to {twice}") out.append( { "number": int(field("number")), "slug": json.loads(field("slug")), "title": json.loads(field("title")), "summary": json.loads(field("summary")), "stories": [ {"title": t, "slug": s, "components": components(t, known)} for t, s in zip(titles, slugs) ], } ) return out def main(): rows = [json.dumps(page, ensure_ascii=False) for page in pages()] text = '{"pages": [\n' + ",\n".join(rows) + "\n]}\n" if sys.argv[1:] == ["--check"]: if OUT.read_text() != text: sys.exit(f"{OUT.relative_to(ROOT)} is stale: run python3 scripts/stories.py") elif sys.argv[1:]: sys.exit("usage: python3 scripts/stories.py [--check]") else: OUT.write_text(text) if __name__ == "__main__": main()