Files
wehub-resource-sync 5f98960d22
Deploy site to GitHub Pages / build (push) Failing after 1s
Deploy site to GitHub Pages / deploy (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:02:56 +08:00

269 lines
11 KiB
Python

#!/usr/bin/env python3
"""Generate assets/languages/*.svg — the README "Language Support" icon grid.
One tile per supported language: a paper card with the language's vector mark and
its name centered underneath. Labels are converted to vector outlines (Archivo,
same pipeline as generate-waitlist.py) so the SVG renders pixel-identical
everywhere: GitHub loads README SVGs in "secure static mode", which blocks
@font-face / web-font loading, so outlines are the only reliable way to ship the
brand typeface. Every tile sits on the brand paper (#f7f6f2), so brand-colored
marks — including near-black ones like Rust — stay legible on GitHub light *and*
dark themes with no media-query tricks.
Glyph sources (fetched at generation time, results checked in):
- simple-icons via https://cdn.simpleicons.org/<slug> (CC0-1.0)
- devicon via jsDelivr, pinned @v2.16.0 (MIT) — Java, C#,
Objective-C, which simple-icons doesn't carry
- hand-drawn in this file: CFML, COBOL, VB.NET (no usable upstream mark)
All trademarks/logos belong to their respective owners; they're used here only
to indicate language support.
Adding a language: append to LANGS, re-run, reference the new SVG in README.md
with a fresh `?v=1`. If you change EXISTING tile bytes, bump that tile's `?v=N`
in README.md in the same commit (GitHub caches README images aggressively).
Requires: fonttools, brotli (pip install fonttools brotli), network access.
Font: @fontsource-variable/archivo (latin, wght axis) from the landing-page package.
Usage: python3 generate-language-tiles.py [--preview /path/to/preview.svg]
"""
import os
import re
import sys
import urllib.request
from fontTools.ttLib import TTFont
from fontTools.varLib.instancer import instantiateVariableFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
from fontTools.misc.transform import Transform
# --- brand palette (mirrors generate-waitlist.py) ---------------------------
PAPER = "#f7f6f2"
HAIRLINE = "#d6d3c8"
INK = "#16150f"
FONT = os.path.join(
os.path.dirname(__file__),
"../../landing-page/node_modules/@fontsource-variable/archivo/files/archivo-latin-wght-normal.woff2",
)
# --- tile geometry (px) ------------------------------------------------------
TILE = 104 # square tile, rx=8 card like the waitlist button
GLYPH_BOX = 44 # logo box, horizontally centered
GLYPH_TOP = 18
LABEL_SIZE = 12.5
LABEL_WT = 640
LABEL_GAP = 12 # glyph box bottom -> label cap top
TRACK = 0.15
DEVICON = "https://cdn.jsdelivr.net/gh/devicons/devicon@v2.16.0/icons/{0}/{0}-{1}.svg"
# (file slug, label, source) — README "Supported Languages" table order.
# source: ("si", slug) simple-icons | ("devicon", name, variant) | ("custom", key)
LANGS = [
("typescript", "TypeScript", ("si", "typescript")),
("javascript", "JavaScript", ("si", "javascript")),
("arkts", "ArkTS", ("si", "harmonyos")),
("python", "Python", ("si", "python")),
("go", "Go", ("si", "go")),
("rust", "Rust", ("si", "rust")),
("java", "Java", ("devicon", "java", "original")),
("csharp", "C#", ("devicon", "csharp", "original")),
("php", "PHP", ("si", "php")),
("ruby", "Ruby", ("si", "ruby")),
("c", "C", ("si", "c")),
("cpp", "C++", ("si", "cplusplus")),
("objective-c", "Objective-C", ("devicon", "objectivec", "plain")),
("metal", "Metal", ("si", "apple")),
("cuda", "CUDA", ("si", "nvidia")),
("swift", "Swift", ("si", "swift")),
("kotlin", "Kotlin", ("si", "kotlin")),
("scala", "Scala", ("si", "scala")),
("dart", "Dart", ("si", "dart")),
("svelte", "Svelte", ("si", "svelte")),
("vue", "Vue", ("si", "vuedotjs")),
("astro", "Astro", ("si", "astro")),
("liquid", "Liquid", ("si", "shopify")),
("delphi", "Delphi", ("si", "delphi")),
("lua", "Lua", ("si", "lua")),
("r", "R", ("si", "r")),
("luau", "Luau", ("si", "luau")),
("cfml", "CFML", ("custom", "cfml")),
("cobol", "COBOL", ("custom", "cobol")),
("vbnet", "VB.NET", ("custom", "vbnet")),
("erlang", "Erlang", ("si", "erlang")),
("solidity", "Solidity", ("si", "solidity")),
("terraform", "Terraform", ("si", "terraform")),
("nix", "Nix", ("si", "nixos")),
]
def fetch(url):
req = urllib.request.Request(url, headers={"User-Agent": "codegraph-assets"})
with urllib.request.urlopen(req, timeout=20) as r:
return r.read().decode("utf-8")
class Outliner:
"""Archivo text -> SVG path outlines, one font instance per weight."""
def __init__(self):
self._by_weight = {}
def _font(self, weight):
if weight not in self._by_weight:
ft = TTFont(FONT)
instantiateVariableFont(ft, {"wght": weight}, inplace=True)
self._by_weight[weight] = (ft, ft.getBestCmap(), ft.getGlyphSet(), ft["hmtx"])
return self._by_weight[weight]
def measure(self, text, size, weight, track=TRACK):
ft, cmap, _, hmtx = self._font(weight)
scale = size / ft["head"].unitsPerEm
w = sum(hmtx[cmap[ord(ch)]][0] * scale + track for ch in text)
return w - track
def cap_height(self, size, weight):
ft, _, _, _ = self._font(weight)
return getattr(ft["OS/2"], "sCapHeight", 700) * size / ft["head"].unitsPerEm
def outline(self, text, size, weight, start_x, baseline_y, track=TRACK):
ft, cmap, glyphs, hmtx = self._font(weight)
scale = size / ft["head"].unitsPerEm
sink = SVGPathPen(glyphs, ntos=lambda v: f"{round(v, 2):g}")
pen_x = start_x
for ch in text:
gname = cmap[ord(ch)]
if ch != " ":
t = Transform(scale, 0, 0, -scale, pen_x, baseline_y)
glyphs[gname].draw(TransformPen(sink, t))
pen_x += hmtx[gname][0] * scale + track
return sink.getCommands()
def centered(self, text, size, weight, center_x, baseline_y, track=TRACK):
w = self.measure(text, size, weight, track)
return self.outline(text, size, weight, center_x - w / 2, baseline_y, track)
def si_glyph(slug):
"""simple-icons: single brand-colored 24x24 path, scaled into the glyph box."""
svg = fetch(f"https://cdn.simpleicons.org/{slug}")
fill = re.search(r'fill="(#[0-9A-Fa-f]{3,8})"', svg).group(1)
d = re.search(r'<path d="([^"]+)"', svg).group(1)
s = round(GLYPH_BOX / 24, 5)
x = (TILE - GLYPH_BOX) / 2
return f'<g transform="translate({x},{GLYPH_TOP}) scale({s})"><path fill="{fill}" d="{d}"/></g>'
def devicon_glyph(name, variant):
"""devicon: multi-path 128x128 markup, embedded verbatim and scaled."""
svg = fetch(DEVICON.format(name, variant))
inner = re.search(r"<svg[^>]*>(.*)</svg>", svg, re.S).group(1).strip()
s = round(GLYPH_BOX / 128, 5)
x = (TILE - GLYPH_BOX) / 2
return f'<g transform="translate({x},{GLYPH_TOP}) scale({s})">{inner}</g>'
def custom_glyph(key, out):
x = (TILE - GLYPH_BOX) / 2 # glyph box left edge (30)
cx = TILE / 2 # 52
cy = GLYPH_TOP + GLYPH_BOX / 2 # glyph box vertical center (40)
if key == "cfml":
# The community CFML mark: a <cf> tag, set in Archivo.
size, wt = 20.0, 700
baseline = round(cy + out.cap_height(size, wt) / 2, 2)
d = out.centered("<cf>", size, wt, cx, baseline, track=0.4)
return f'<path fill="#1b5ea6" d="{d}"/>'
if key == "cobol":
# A punched card: manila stock, clipped corner, punched rows.
w, h = GLYPH_BOX, 28
top, cut = round(cy - h / 2, 2), 7
holes = []
for row in range(3):
hy = top + 7.4 + row * 6.4
for col in range(7):
if (row * 3 + col * 5) % 4 != 0: # deterministic "data" pattern
holes.append(
f'<rect x="{round(x + 4.4 + col * 5.4, 2)}" y="{round(hy, 2)}" width="2.4" height="4" fill="#6b5d33"/>'
)
card = (
f'M{x + cut},{top} H{x + w} V{top + h} H{x} V{top + cut} Z'
)
return (
f'<path d="{card}" fill="#ecdfb1" stroke="#b3a06a" stroke-width="1" stroke-linejoin="miter"/>'
+ "".join(holes)
)
if key == "vbnet":
# The .NET-purple badge with a VB monogram.
size, wt = 16.5, 720
baseline = round(cy + out.cap_height(size, wt) / 2, 2)
d = out.centered("VB", size, wt, cx, baseline, track=0.6)
return (
f'<rect x="{x}" y="{GLYPH_TOP}" width="{GLYPH_BOX}" height="{GLYPH_BOX}" rx="6" fill="#512bd4"/>'
f'<path fill="{PAPER}" d="{d}"/>'
)
raise KeyError(key)
def main():
preview_path = None
if "--preview" in sys.argv:
preview_path = sys.argv[sys.argv.index("--preview") + 1]
out = Outliner()
label_baseline = round(GLYPH_TOP + GLYPH_BOX + LABEL_GAP + out.cap_height(LABEL_SIZE, LABEL_WT), 2)
out_dir = os.path.join(os.path.dirname(__file__), "languages")
os.makedirs(out_dir, exist_ok=True)
bodies = {}
for slug, label, source in LANGS:
if source[0] == "si":
glyph = si_glyph(source[1])
elif source[0] == "devicon":
glyph = devicon_glyph(source[1], source[2])
else:
glyph = custom_glyph(source[1], out)
label_d = out.centered(label, LABEL_SIZE, LABEL_WT, TILE / 2, label_baseline)
body = (
f'<rect x="0.5" y="0.5" width="{TILE - 1}" height="{TILE - 1}" rx="8" fill="{PAPER}" stroke="{HAIRLINE}"/>\n'
f" {glyph}\n"
f' <path fill="{INK}" d="{label_d}"/>'
)
bodies[slug] = body
svg = (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{TILE}" height="{TILE}" '
f'viewBox="0 0 {TILE} {TILE}" role="img" aria-label="{label}">\n'
f" <title>{label}</title>\n"
f" {body}\n"
f"</svg>\n"
)
path = os.path.join(out_dir, f"{slug}.svg")
with open(path, "w") as fh:
fh.write(svg)
print(f"wrote {os.path.relpath(path, os.path.dirname(__file__))} ({len(svg)}b) {label}")
if preview_path:
cols, pad = 7, 8
rows = -(-len(LANGS) // cols)
w = cols * (TILE + pad) + pad
h = rows * (TILE + pad) + pad
cells = []
for i, (slug, _, _) in enumerate(LANGS):
tx = pad + (i % cols) * (TILE + pad)
ty = pad + (i // cols) * (TILE + pad)
cells.append(f'<g transform="translate({tx},{ty})">{bodies[slug]}</g>')
light = "".join(cells)
with open(preview_path, "w") as fh:
fh.write(
f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h * 2}" viewBox="0 0 {w} {h * 2}">'
f'<rect width="{w}" height="{h}" fill="#ffffff"/>{light}'
f'<g transform="translate(0,{h})"><rect width="{w}" height="{h}" fill="#0d1117"/>{light}</g>'
f"</svg>\n"
)
print(f"wrote preview {preview_path} (top: light bg, bottom: dark bg)")
if __name__ == "__main__":
main()