# Copyright (c) 2026 Feng Ruohang # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Pure, self-contained SVG rendering for SILO's README cards.""" from datetime import date, timedelta from html import escape import math import xml.etree.ElementTree as ET themes = { 'light': dict(bg='#ffffff', wash='#f2f7fc', edge='#d9e3ee', ink='#16222e', muted='#62758a', blue='#1d588c', copper='#b4762e', grid='#e5edf5', line='#2b6ca3', ring='#dce5ef', field='#f7f9fc', label='#3d4e61'), 'dark': dict(bg='#101923', wash='#152738', edge='#2b3c50', ink='#e8eef6', muted='#93a3b8', blue='#7fb8e8', copper='#e0a35c', grid='#263749', line='#5da2dd', ring='#3a4e63', field='#0b1119', label='#b6c2d2'), } def read_emblem(path): emblem = ET.parse(path).getroot() body = ''.join(ET.tostring(child, encoding='unicode') for child in emblem if child.tag.rsplit('}', 1)[-1] in ('defs', 'g')) return '\n'.join(line.rstrip() for line in body.splitlines()).strip() def txt(x, y, value, size=14, color=None, weight=400, anchor='start', mono=False, spacing=None): family = 'Menlo,Consolas,monospace' if mono else 'Arial,Helvetica,sans-serif' extra = f' letter-spacing="{spacing}"' if spacing is not None else '' return (f'' f'{escape(str(value))}') def start(height, theme, title, description, emblem_body): t = themes[theme] return [f'', f'{escape(title)}{escape(description)}', '' f'' f'' '' f'' f'' '' f'' f'' '', f'', f'{emblem_body}'] def heading(parts, t, eyebrow, title, subtitle, value, value_label): parts.extend([ txt(76, 43, eyebrow, 11, t['muted'], 600, mono=True, spacing=1.7), txt(40, 94, title, 32, t['ink'], 700), txt(41, 123, subtitle, 14, t['muted']), txt(958, 89, f'{value:,}', 45, t['ink'], 700, anchor='end'), txt(957, 114, value_label, 10, t['muted'], 600, anchor='end', mono=True, spacing=1.5), f'', ]) def contributors(theme, people, snapshot, emblem_body): height = 206 + 76 * math.ceil(len(people) / 10) t = themes[theme] parts = start(height, theme, f'SILO community — {len(people)} contributors', f'The existing SILO community roll, including code, proposals and reports across related projects. ' f'Gold rings retain the existing significant-contribution designation. Snapshot {snapshot}.', emblem_body) heading(parts, t, 'SILO / COMMUNITY', 'Contributors', 'Code, proposals & reports across SILO and related projects', len(people), 'COMMUNITY CONTRIBUTORS') for row in range(math.ceil(len(people) / 10)): group = people[row * 10:(row + 1) * 10] row_width = len(group) * 91 for col, person in enumerate(group): x = (1000 - row_width) / 2 + col * 91 + 45.5 y = 199 + row * 76 identifier = f'avatar-{row}-{col}' featured = bool(person.get('featured')) parts.append(f'@{escape(person["handle"])} — {escape(person["what"])}') parts.append(f'') if featured: parts.append(f'') if person.get('avatarDataUrl'): parts.append(f'') else: parts.append(f'') parts.append(txt(x, y + 9, person['handle'][0].upper(), 26, t['ink'], 700, 'middle')) parts.append(f'') parts.extend([ f'', f'', txt(61, height-17, 'Gold rings mark significant contributions', 12, t['muted']), txt(959, height-17, f'AS OF {snapshot}', 10, t['muted'], 500, 'end', mono=True, spacing=.6), '', ]) return ''.join(parts) def stars(theme, history, snapshot, emblem_body): points = history['points'] star_count = points[-1]['stars'] t = themes[theme] provenance = ('Initial history reconstructed · Daily totals since ' + history['bootstrap']['through'] + ' · UTC' if history['bootstrap']['reconstructed'] else 'Observed daily star totals · UTC') parts = start(558, theme, f'SILO star history — {star_count:,} stars', f'GitHub repository pgsty/silo. {star_count:,} stars as of {snapshot}. ' + provenance, emblem_body) heading(parts, t, 'SILO / GITHUB', 'Star History', 'pgsty/silo', star_count, 'GITHUB STARS') left, right, top, bottom = 76, 958, 177, 440 begin = date.fromisoformat(points[0]['date']) end = date.fromisoformat(points[-1]['date']) days = max(1, (end - begin).days) maximum = max(500, math.ceil(max(p['stars'] for p in points) / 500) * 500) tick_step = 10 ** max(0, int(math.log10(maximum))) xy = lambda day, n: (left + (right-left)*(date.fromisoformat(day)-begin).days / days, bottom-(bottom-top)*n/maximum) for value in range(0, maximum+1, tick_step): y = xy(points[0]['date'], value)[1] parts.append(f'') parts.append(txt(left-16, round(y+4, 2), f'{value / 1000:g}k' if value >= 1000 else str(value), 12, t['muted'], anchor='end')) dates = sorted({begin + timedelta(days=round((end-begin).days*i/5)) for i in range(6)}) ticks = [(d.isoformat(), d.strftime('%b %Y') if days > 90 else d.strftime('%b %d')) for d in dates] for day, label in ticks: x = xy(day, 0)[0] parts.append(f'') anchor = 'start' if day == points[0]['date'] else 'end' if day == snapshot else 'middle' parts.append(txt(round(x, 2), 466, label, 12, t['muted'], anchor=anchor)) coords = [xy(p['date'], p['stars']) for p in points] line = 'M' + ' L'.join(f'{x:.2f} {y:.2f}' for x, y in coords) area = line + f' L{coords[-1][0]:.2f} {bottom} L{left} {bottom} Z' parts.extend([ f'', f'', f'', ]) x, y = coords[-1] parts.extend([ f'', f'', f'', txt(40, 516, f'{begin:%b %Y} — {end:%b %Y}'.upper(), 10, t['muted'], 500, mono=True, spacing=.7), txt(959, 516, f'SNAPSHOT {snapshot}', 10, t['muted'], 500, 'end', mono=True, spacing=.6), txt(40, 539, provenance, 11, t['muted']), '', ]) return ''.join(parts)