{"slug": "explain-diff", "title": "explain-diff", "summary": "A developer created a Python script called render.py that generates self-contained HTML pages for the explain-diff skill, reducing token waste by separating content from boilerplate CSS and JavaScript. The tool takes a JSON spec with only the variable content and renders a complete page with randomized quiz options.", "body_md": "|\n#!/usr/bin/env python3 |\n|\n\"\"\" |\n|\nrender.py — render a structured explain-diff spec into the self-contained HTML |\n|\npage format used by the `explain-diff-html` command (see ~/.claude/commands/ |\n|\nexplain-diff-html.md, recipe from |\n|\nhttps://gist.github.com/geoffreylitt/a29df1b5f9865506e8952488eac3d524). |\n|\n|\n|\nWhy this exists: the CSS, quiz JavaScript, and page scaffolding are identical |\n|\nacross every invocation of the explain-diff skill — only the content (prose, |\n|\ndiagrams, quiz questions) actually changes per diff. Regenerating the full |\n|\n~250 lines of boilerplate CSS/JS by hand every time wastes tokens. This script |\n|\ntakes a small JSON spec with just the content and renders the final page. |\n|\n|\n|\nUsage: |\n|\npython render.py spec.json [-o output.html] |\n|\n|\n|\nIf -o is omitted, writes to <dir-of-spec>/YYYY-MM-DD-<slug>.html (matching the |\n|\nskill's filename convention), where <slug> comes from the spec's \"slug\" field. |\n|\n|\n|\nSpec format (JSON): |\n|\n{ |\n|\n\"title\": \"Rewriting the retry logic: exponential backoff with jitter\", |\n|\n\"subtitle\": \"Prepared 2026-07-15 · PR #482\", |\n|\n\"slug\": \"retry-backoff-refactor\", |\n|\n\"sections\": [ |\n|\n{\"id\": \"background\", \"heading\": \"Background\", \"html\": \"<p>...</p>\"}, |\n|\n{\"id\": \"intuition\", \"heading\": \"Intuition\", \"html\": \"<p>...</p><div class=\\\"diagram\\\">...</div>\"}, |\n|\n{\"id\": \"code\", \"heading\": \"Code walkthrough\", \"html\": \"<pre><code>...</code></pre>\"} |\n|\n], |\n|\n\"quiz\": [ |\n|\n{ |\n|\n\"question\": \"Why did the first retry attempt fire immediately instead of waiting?\", |\n|\n\"options\": [ |\n|\n{\"text\": \"The jitter calculation returned a negative delay.\", \"correct\": false}, |\n|\n{\"text\": \"The base delay was multiplied after the first attempt, not before it.\", \"correct\": true} |\n|\n] |\n|\n} |\n|\n] |\n|\n} |\n|\n|\n|\nOption order within each quiz question is randomized by the renderer at render time — |\n|\nlist them in whatever order reads naturally when writing the spec; don't try to |\n|\nmanually vary position to \"seem random\", the script already guarantees it. |\n|\n|\n|\nThe \"html\" fields are raw HTML — write real markup (headings, <pre> blocks, |\n|\ntables, \".diagram\"/\".callout\" divs per the skill's CSS classes below), not |\n|\nmarkdown. This keeps the script a pure template renderer; all the writing |\n|\njudgment (what to explain, which diagrams to draw) still belongs to the LLM |\n|\nfollowing the explain-diff-html recipe, same as before — this just removes |\n|\nthe repetitive part. |\n|\n\"\"\" |\n|\nimport argparse |\n|\nimport datetime |\n|\nimport html |\n|\nimport json |\n|\nimport random |\n|\nimport re |\n|\nimport sys |\n|\nfrom pathlib import Path |\n|\n|\n|\nCSS = \"\"\" |\n|\n:root { |\n|\n--bg: #fafaf8; --fg: #1a1a1a; --accent: #b5541f; --muted: #6b6b6b; |\n|\n--code-bg: #282c34; --code-fg: #e6e6e6; --callout-bg: #fff4e8; --border: #e0ddd6; |\n|\n} |\n|\nbody { font-family: Georgia, 'Times New Roman', serif; background: var(--bg); color: var(--fg); |\n|\nmax-width: 820px; margin: 0 auto; padding: 2rem 1.5rem 6rem; line-height: 1.65; } |\n|\nh1 { font-size: 1.9rem; border-bottom: 3px solid var(--accent); padding-bottom: .5rem; } |\n|\nh2 { font-size: 1.4rem; margin-top: 3rem; color: var(--accent); } |\n|\nh3 { font-size: 1.1rem; margin-top: 1.8rem; } |\n|\ncode { font-family: 'SF Mono', Consolas, monospace; background: #eee; padding: .1rem .3rem; border-radius: 3px; font-size: .92em; } |\n|\npre { background: var(--code-bg); color: var(--code-fg); padding: 1rem 1.2rem; border-radius: 8px; |\n|\noverflow-x: auto; white-space: pre-wrap; font-family: 'SF Mono', Consolas, monospace; font-size: .88rem; line-height: 1.5; } |\n|\npre code { background: none; padding: 0; color: inherit; } |\n|\n.callout { background: var(--callout-bg); border-left: 4px solid var(--accent); padding: .9rem 1.2rem; |\n|\nborder-radius: 0 6px 6px 0; margin: 1.2rem 0; } |\n|\n.toc { background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.5rem; margin: 1.5rem 0; } |\n|\n.toc a { color: var(--accent); text-decoration: none; } |\n|\n.toc ul { margin: .3rem 0; } |\n|\n.diagram { background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 1.2rem; |\n|\nmargin: 1.2rem 0; font-family: 'SF Mono', Consolas, monospace; font-size: .85rem; } |\n|\n.flow { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; justify-content: center; padding: .5rem 0; } |\n|\n.box { border: 2px solid var(--accent); border-radius: 8px; padding: .6rem 1rem; background: #fdf6ee; text-align: center; min-width: 120px; } |\n|\n.box.fail { border-color: #b91c1c; background: #fef2f2; } |\n|\n.arrow { font-size: 1.4rem; color: var(--muted); } |\n|\ntable { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .92rem; } |\n|\nth, td { border: 1px solid var(--border); padding: .5rem .7rem; text-align: left; } |\n|\nth { background: #f0ede6; } |\n|\n.quiz-q { background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 1.2rem 1.5rem; margin: 1.2rem 0; } |\n|\n.quiz-opt { display: block; width: 100%; text-align: left; padding: .6rem 1rem; margin: .4rem 0; |\n|\nborder: 1px solid var(--border); border-radius: 6px; background: #fff; cursor: pointer; font-family: inherit; font-size: .95rem; } |\n|\n.quiz-opt:hover { background: #f5f2ec; } |\n|\n.feedback { display: none; margin-top: .6rem; padding: .6rem 1rem; border-radius: 6px; font-size: .9rem; } |\n|\n.feedback.correct { background: #ecfdf3; color: #166534; border-left: 3px solid #16a34a; } |\n|\n.feedback.incorrect { background: #fef2f2; color: #991b1b; border-left: 3px solid #dc2626; } |\n|\n.badge { display: inline-block; font-size: .75rem; padding: .15rem .5rem; border-radius: 10px; font-family: sans-serif; } |\n|\n.badge.new { background: #dcfce7; color: #166534; } |\n|\n@media (max-width: 600px) { body { padding: 1rem; } .flow { flex-direction: column; } } |\n|\n\"\"\" |\n|\n|\n|\nQUIZ_JS = \"\"\" |\n|\ndocument.querySelectorAll('.quiz-q').forEach(q => { |\n|\nq.querySelectorAll('.quiz-opt').forEach(opt => { |\n|\nopt.addEventListener('click', () => { |\n|\nconst correct = opt.dataset.correct === 'true'; |\n|\nlet fb = opt.nextElementSibling; |\n|\nif (!fb || !fb.classList.contains('feedback')) { |\n|\nfb = document.createElement('div'); |\n|\nfb.className = 'feedback'; |\n|\nopt.insertAdjacentElement('afterend', fb); |\n|\n} |\n|\nfb.textContent = correct ? '\\\\u2705 Correct.' : '\\\\u274c Not quite \\\\u2014 reread the section above.'; |\n|\nfb.className = 'feedback ' + (correct ? 'correct' : 'incorrect'); |\n|\nfb.style.display = 'block'; |\n|\n}); |\n|\n}); |\n|\n}); |\n|\n\"\"\" |\n|\n|\n|\n|\n|\ndef slugify(text: str) -> str: |\n|\nreturn re.sub(r\"[^a-z0-9]+\", \"-\", text.lower()).strip(\"-\") |\n|\n|\n|\n|\n|\ndef render(spec: dict) -> str: |\n|\ntitle = spec[\"title\"] |\n|\nsubtitle = spec.get(\"subtitle\", \"\") |\n|\nsections = spec.get(\"sections\", []) |\n|\nquiz = spec.get(\"quiz\", []) |\n|\n|\n|\ntoc_items = \"\\n\".join( |\n|\nf' <li><a href=\"#{s[\"id\"]}\">{html.escape(s[\"heading\"])}</a></li>' for s in sections |\n|\n) |\n|\nif quiz: |\n|\ntoc_items += '\\n <li><a href=\"#quiz\">Quiz</a></li>' |\n|\n|\n|\nbody_sections = \"\\n\\n\".join( |\n|\nf'<h2 id=\"{s[\"id\"]}\">{html.escape(s[\"heading\"])}</h2>\\n{s[\"html\"]}' for s in sections |\n|\n) |\n|\n|\n|\nquiz_html = \"\" |\n|\nif quiz: |\n|\nblocks = [] |\n|\nfor q in quiz: |\n|\noptions = list(q[\"options\"]) |\n|\nrandom.shuffle(options) |\n|\nopts = \"\\n\".join( |\n|\nf'<button class=\"quiz-opt\" data-correct=\"{\"true\" if o[\"correct\"] else \"false\"}\">{o[\"text\"]}</button>' |\n|\nfor o in options |\n|\n) |\n|\nblocks.append(f'<div class=\"quiz-q\">\\n<p><strong>{html.escape(q[\"question\"])}</strong></p>\\n{opts}\\n</div>') |\n|\nquiz_html = '<h2 id=\"quiz\">Quiz</h2>\\n\\n' + \"\\n\\n\".join(blocks) |\n|\n|\n|\nreturn f\"\"\"<!DOCTYPE html> |\n|\n<html lang=\"en\"> |\n|\n<head> |\n|\n<meta charset=\"UTF-8\"> |\n|\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> |\n|\n<title>{html.escape(title)}</title> |\n|\n<style>{CSS}</style> |\n|\n</head> |\n|\n<body> |\n|\n|\n|\n<h1>{html.escape(title)}</h1> |\n|\n{f'<p style=\"color:var(--muted); margin-top:-.5rem;\">{html.escape(subtitle)}</p>' if subtitle else ''} |\n|\n|\n|\n<div class=\"toc\"> |\n|\n<strong>Contents</strong> |\n|\n<ul> |\n|\n{toc_items} |\n|\n</ul> |\n|\n</div> |\n|\n|\n|\n{body_sections} |\n|\n|\n|\n{quiz_html} |\n|\n|\n|\n<script>{QUIZ_JS}</script> |\n|\n|\n|\n</body> |\n|\n</html> |\n|\n\"\"\" |\n|\n|\n|\n|\n|\ndef main(): |\n|\nap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) |\n|\nap.add_argument(\"spec\", type=Path, help=\"path to the JSON content spec\") |\n|\nap.add_argument(\"-o\", \"--output\", type=Path, default=None, help=\"output HTML path\") |\n|\nargs = ap.parse_args() |\n|\n|\n|\nspec = json.loads(args.spec.read_text(encoding=\"utf-8\")) |\n|\nout_html = render(spec) |\n|\n|\n|\nif args.output: |\n|\nout_path = args.output |\n|\nelse: |\n|\ndate_prefix = datetime.date.today().strftime(\"%Y-%m-%d\") |\n|\nslug = spec.get(\"slug\") or slugify(spec[\"title\"]) |\n|\nout_path = Path(f\"/tmp/{date_prefix}-explanation-{slug}.html\") |\n|\n|\n|\nout_path.write_text(out_html, encoding=\"utf-8\") |\n|\nprint(str(out_path)) |\n|\n|\n|\n|\n|\nif __name__ == \"__main__\": |\n|\nmain() |", "url": "https://wpnews.pro/news/explain-diff", "canonical_source": "https://gist.github.com/ankitg12/8e808d387799de4e9839bc393f8e6405", "published_at": "2026-07-15 08:27:57+00:00", "updated_at": "2026-07-17 13:27:41.045173+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/explain-diff", "markdown": "https://wpnews.pro/news/explain-diff.md", "text": "https://wpnews.pro/news/explain-diff.txt", "jsonld": "https://wpnews.pro/news/explain-diff.jsonld"}}