Skip to content

Commit 804d62f

Browse files
brunoborgesCopilot
andcommitted
Sync generate.py with Java version and add generateog.py
generate.py: - Fix render_social_share to include category in share URLs - Add ogImage token for per-pattern OG meta tags generateog.py (new): - Python equivalent of generateog.java - Syntax-highlighted SVG+PNG cards (1200x630, 2x PNG) - Same layout, palette, and dynamic font sizing as Java version - Uses cairosvg for PNG conversion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0b72f97 commit 804d62f

File tree

2 files changed

+338
-3
lines changed

2 files changed

+338
-3
lines changed

html-generators/generate.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,9 @@ def render_proof_section(data, strings):
367367
)
368368

369369

370-
def render_social_share(tpl, slug, title, strings):
370+
def render_social_share(tpl, category, slug, title, strings):
371371
"""Render social share URLs."""
372-
encoded_url = url_encode(f"{BASE_URL}/{slug}.html")
372+
encoded_url = url_encode(f"{BASE_URL}/{category}/{slug}.html")
373373
encoded_text = url_encode(f"{title} \u2013 java.evolved")
374374
return replace_tokens(tpl, {
375375
"encodedUrl": encoded_url,
@@ -550,8 +550,9 @@ def generate_html(templates, data, all_snippets, extra_tokens, locale):
550550
"relatedCards": render_related_section(
551551
templates["related_card"], data, all_snippets, locale, extra_tokens
552552
),
553+
"ogImage": f"{BASE_URL}/og/{cat}/{slug}.png",
553554
"socialShare": render_social_share(
554-
templates["social_share"], slug, data["title"], extra_tokens
555+
templates["social_share"], cat, slug, data["title"], extra_tokens
555556
),
556557
})
557558

html-generators/generateog.py

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate Open Graph SVG+PNG cards (1200×630) for each pattern.
4+
Light theme, side-by-side Old/Modern code, slug title at top.
5+
Python equivalent of generateog.java — produces identical output.
6+
7+
Usage: python html-generators/generateog.py [category/slug]
8+
No arguments → generate all patterns.
9+
10+
Requires: cairosvg (pip install cairosvg)
11+
"""
12+
13+
import json
14+
import os
15+
import re
16+
import sys
17+
import glob as glob_mod
18+
from collections import OrderedDict
19+
20+
try:
21+
import yaml
22+
except ImportError:
23+
yaml = None
24+
25+
try:
26+
import cairosvg
27+
except ImportError:
28+
cairosvg = None
29+
30+
CONTENT_DIR = "content"
31+
OUTPUT_DIR = "site/og"
32+
CATEGORIES_FILE = "html-generators/categories.properties"
33+
34+
# ── Light-theme palette ─────────────────────────────────────────────────
35+
BG = "#ffffff"
36+
BORDER = "#d8d8e0"
37+
TEXT = "#1a1a2e"
38+
TEXT_MUTED = "#6b7280"
39+
OLD_BG = "#fef2f2"
40+
MODERN_BG = "#eff6ff"
41+
OLD_ACCENT = "#dc2626"
42+
GREEN = "#059669"
43+
ACCENT = "#6366f1"
44+
BADGE_BG = "#f3f4f6"
45+
46+
# ── Syntax highlight colors (VS Code light-inspired) ────────────────────
47+
SYN_KEYWORD = "#7c3aed"
48+
SYN_TYPE = "#0e7490"
49+
SYN_STRING = "#059669"
50+
SYN_COMMENT = "#6b7280"
51+
SYN_ANNOTATION = "#b45309"
52+
SYN_NUMBER = "#c2410c"
53+
SYN_DEFAULT = "#1a1a2e"
54+
55+
JAVA_KEYWORDS = {
56+
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char",
57+
"class", "const", "continue", "default", "do", "double", "else", "enum",
58+
"extends", "final", "finally", "float", "for", "goto", "if", "implements",
59+
"import", "instanceof", "int", "interface", "long", "native", "new", "null",
60+
"package", "private", "protected", "public", "record", "return", "sealed",
61+
"short", "static", "strictfp", "super", "switch", "synchronized", "this",
62+
"throw", "throws", "transient", "try", "var", "void", "volatile", "when",
63+
"while", "with", "yield", "permits", "non-sealed", "module", "open", "opens",
64+
"requires", "exports", "provides", "to", "uses", "transitive",
65+
"true", "false",
66+
}
67+
68+
SYN_PATTERN = re.compile(
69+
r"(?P<comment>//.*)|"
70+
r"(?P<blockcomment>/\*.*?\*/)|"
71+
r"(?P<annotation>@\w+)|"
72+
r'(?P<string>"""[\s\S]*?"""|"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\')|'
73+
r"(?P<number>\b\d[\d_.]*[dDfFlL]?\b)|"
74+
r"(?P<word>\b[A-Za-z_]\w*\b)|"
75+
r"(?P<other>[^\s])"
76+
)
77+
78+
# ── Dimensions ──────────────────────────────────────────────────────────
79+
W = 1200
80+
H = 630
81+
PAD = 40
82+
HEADER_H = 100
83+
FOOTER_H = 56
84+
CODE_TOP = HEADER_H
85+
CODE_H = H - HEADER_H - FOOTER_H
86+
COL_W = (W - PAD * 2 - 20) // 2
87+
CODE_PAD = 14
88+
LABEL_H = 32
89+
USABLE_W = COL_W - CODE_PAD * 2
90+
USABLE_H = CODE_H - LABEL_H - CODE_PAD
91+
CHAR_WIDTH_RATIO = 0.6
92+
LINE_HEIGHT_RATIO = 1.55
93+
MIN_CODE_FONT = 9
94+
MAX_CODE_FONT = 16
95+
96+
97+
# ── Helpers ─────────────────────────────────────────────────────────────
98+
99+
def load_properties(path):
100+
props = OrderedDict()
101+
with open(path) as f:
102+
for line in f:
103+
line = line.strip()
104+
if not line or line.startswith("#"):
105+
continue
106+
idx = line.find("=")
107+
if idx > 0:
108+
props[line[:idx].strip()] = line[idx + 1:].strip()
109+
return props
110+
111+
112+
CATEGORY_DISPLAY = load_properties(CATEGORIES_FILE)
113+
114+
115+
def read_auto(path):
116+
with open(path) as f:
117+
if path.endswith((".yaml", ".yml")):
118+
if yaml is None:
119+
raise ImportError("PyYAML is required for YAML files: pip install pyyaml")
120+
return yaml.safe_load(f)
121+
return json.load(f)
122+
123+
124+
def xml_escape(s):
125+
if s is None:
126+
return ""
127+
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
128+
.replace('"', "&quot;").replace("'", "&apos;"))
129+
130+
131+
def load_all_snippets():
132+
snippets = OrderedDict()
133+
for cat in CATEGORY_DISPLAY:
134+
cat_dir = os.path.join(CONTENT_DIR, cat)
135+
if not os.path.isdir(cat_dir):
136+
continue
137+
files = []
138+
for ext in ("json", "yaml", "yml"):
139+
files.extend(glob_mod.glob(os.path.join(cat_dir, f"*.{ext}")))
140+
files.sort()
141+
for path in files:
142+
data = read_auto(path)
143+
key = f"{data['category']}/{data['slug']}"
144+
snippets[key] = data
145+
return snippets
146+
147+
148+
# ── Syntax highlighting ─────────────────────────────────────────────────
149+
150+
def highlight_line(line):
151+
if line == "...":
152+
return xml_escape(line)
153+
result = []
154+
last = 0
155+
for m in SYN_PATTERN.finditer(line):
156+
if m.start() > last:
157+
result.append(xml_escape(line[last:m.start()]))
158+
last = m.end()
159+
token = m.group()
160+
color = None
161+
if m.group("comment") or m.group("blockcomment"):
162+
color = SYN_COMMENT
163+
elif m.group("annotation"):
164+
color = SYN_ANNOTATION
165+
elif m.group("string"):
166+
color = SYN_STRING
167+
elif m.group("number"):
168+
color = SYN_NUMBER
169+
elif m.group("word"):
170+
if token in JAVA_KEYWORDS:
171+
color = SYN_KEYWORD
172+
elif token[0].isupper():
173+
color = SYN_TYPE
174+
if color:
175+
result.append(f'<tspan fill="{color}">{xml_escape(token)}</tspan>')
176+
else:
177+
result.append(xml_escape(token))
178+
if last < len(line):
179+
result.append(xml_escape(line[last:]))
180+
return "".join(result)
181+
182+
183+
# ── SVG rendering ───────────────────────────────────────────────────────
184+
185+
def best_font_size(old_lines, modern_lines):
186+
max_chars = max(
187+
max((len(l) for l in old_lines), default=1),
188+
max((len(l) for l in modern_lines), default=1),
189+
)
190+
max_lines = max(len(old_lines), len(modern_lines))
191+
by_width = int(USABLE_W / (max_chars * CHAR_WIDTH_RATIO))
192+
by_height = int(USABLE_H / (max_lines * LINE_HEIGHT_RATIO))
193+
return max(MIN_CODE_FONT, min(MAX_CODE_FONT, min(by_width, by_height)))
194+
195+
196+
def fit_lines(lines, font_size):
197+
line_h = int(font_size * LINE_HEIGHT_RATIO)
198+
max_lines = USABLE_H // line_h
199+
if len(lines) <= max_lines:
200+
return lines
201+
truncated = list(lines[:max_lines - 1])
202+
truncated.append("...")
203+
return truncated
204+
205+
206+
def render_code_block(lines, x, y, line_h):
207+
parts = []
208+
for i, line in enumerate(lines):
209+
parts.append(
210+
f' <text x="{x}" y="{y + i * line_h}" class="code" xml:space="preserve">'
211+
f'{highlight_line(line)}</text>\n'
212+
)
213+
return "".join(parts)
214+
215+
216+
def generate_svg(data):
217+
left_x = PAD
218+
right_x = PAD + COL_W + 20
219+
label_y = CODE_TOP + 26
220+
code_y = CODE_TOP + 52
221+
222+
old_lines = data["oldCode"].split("\n")
223+
modern_lines = data["modernCode"].split("\n")
224+
225+
font_size = best_font_size(old_lines, modern_lines)
226+
line_h = int(font_size * LINE_HEIGHT_RATIO)
227+
228+
old_lines = fit_lines(old_lines, font_size)
229+
modern_lines = fit_lines(modern_lines, font_size)
230+
231+
cat_display = CATEGORY_DISPLAY.get(data["category"], data["category"])
232+
badge_width = len(cat_display) * 8 + 16
233+
234+
old_code_svg = render_code_block(old_lines, left_x + 14, code_y, line_h)
235+
modern_code_svg = render_code_block(modern_lines, right_x + 14, code_y, line_h)
236+
237+
return f"""<?xml version="1.0" encoding="UTF-8"?>
238+
<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">
239+
<defs>
240+
<style>
241+
.title {{ font: 700 24px/1 'Inter', sans-serif; fill: {TEXT}; }}
242+
.category {{ font: 600 13px/1 'Inter', sans-serif; fill: {TEXT_MUTED}; }}
243+
.label {{ font: 600 11px/1 'Inter', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; }}
244+
.code {{ font: 400 {font_size}px/1 'JetBrains Mono', monospace; fill: {TEXT}; }}
245+
.footer {{ font: 500 13px/1 'Inter', sans-serif; fill: {TEXT_MUTED}; }}
246+
.brand {{ font: 700 14px/1 'Inter', sans-serif; fill: {ACCENT}; }}
247+
</style>
248+
<clipPath id="clip-left">
249+
<rect x="{left_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8"/>
250+
</clipPath>
251+
<clipPath id="clip-right">
252+
<rect x="{right_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8"/>
253+
</clipPath>
254+
</defs>
255+
256+
<!-- Background -->
257+
<rect width="{W}" height="{H}" rx="16" fill="{BG}"/>
258+
<rect x="0.5" y="0.5" width="{W - 1}" height="{H - 1}" rx="16" fill="none" stroke="{BORDER}" stroke-width="1"/>
259+
260+
<!-- Header: category badge + title -->
261+
<rect x="{PAD}" y="28" width="{badge_width}" height="22" rx="6" fill="{BADGE_BG}"/>
262+
<text x="{PAD + 8}" y="43" class="category">{xml_escape(cat_display)}</text>
263+
<text x="{PAD}" y="76" class="title">{xml_escape(data['title'])}</text>
264+
265+
<!-- Left panel: Old code -->
266+
<rect x="{left_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8" fill="{OLD_BG}"/>
267+
<rect x="{left_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8" fill="none" stroke="{BORDER}" stroke-width="0.5"/>
268+
<text x="{left_x + 14}" y="{label_y}" class="label" fill="{OLD_ACCENT}">\u2717 {xml_escape(data['oldLabel'])}</text>
269+
<g clip-path="url(#clip-left)">
270+
{old_code_svg} </g>
271+
272+
<!-- Right panel: Modern code -->
273+
<rect x="{right_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8" fill="{MODERN_BG}"/>
274+
<rect x="{right_x}" y="{CODE_TOP}" width="{COL_W}" height="{CODE_H}" rx="8" fill="none" stroke="{BORDER}" stroke-width="0.5"/>
275+
<text x="{right_x + 14}" y="{label_y}" class="label" fill="{GREEN}">\u2713 {xml_escape(data['modernLabel'])}</text>
276+
<g clip-path="url(#clip-right)">
277+
{modern_code_svg} </g>
278+
279+
<!-- Footer -->
280+
<text x="{PAD}" y="{H - 22}" class="footer">JDK {data['jdkVersion']}+</text>
281+
<text x="{W - PAD}" y="{H - 22}" class="brand" text-anchor="end">javaevolved.github.io</text>
282+
</svg>
283+
"""
284+
285+
286+
def svg_to_png(svg_content, png_path):
287+
if cairosvg is None:
288+
raise ImportError("cairosvg is required for PNG generation: pip install cairosvg")
289+
cairosvg.svg2png(
290+
bytestring=svg_content.encode("utf-8"),
291+
write_to=png_path,
292+
output_width=W * 2,
293+
output_height=H * 2,
294+
)
295+
296+
297+
# ── Main ────────────────────────────────────────────────────────────────
298+
299+
def main():
300+
all_snippets = load_all_snippets()
301+
print(f"Loaded {len(all_snippets)} snippets")
302+
303+
# Filter to a single slug if provided
304+
if len(sys.argv) > 1:
305+
key = sys.argv[1]
306+
if key not in all_snippets:
307+
print(f"Unknown pattern: {key}")
308+
print(f"Available: {', '.join(all_snippets.keys())}")
309+
sys.exit(1)
310+
targets = {key: all_snippets[key]}
311+
else:
312+
targets = all_snippets
313+
314+
count = 0
315+
for key, data in targets.items():
316+
cat = data["category"]
317+
slug = data["slug"]
318+
out_dir = os.path.join(OUTPUT_DIR, cat)
319+
os.makedirs(out_dir, exist_ok=True)
320+
321+
svg = generate_svg(data)
322+
svg_path = os.path.join(out_dir, f"{slug}.svg")
323+
with open(svg_path, "w") as f:
324+
f.write(svg)
325+
326+
png_path = os.path.join(out_dir, f"{slug}.png")
327+
svg_to_png(svg, png_path)
328+
count += 1
329+
330+
print(f"Generated {count} SVG+PNG card(s) in {OUTPUT_DIR}/")
331+
332+
333+
if __name__ == "__main__":
334+
main()

0 commit comments

Comments
 (0)