|
| 1 | +// easy-github-profile — github.com/BerkaySevinc/easy-github-profile |
| 2 | +// Copyright (c) 2025 BerkaySevinc — MIT License |
| 3 | + |
| 4 | +const { writeFileSync } = require('fs'); |
| 5 | +const { join } = require('path'); |
| 6 | + |
| 7 | +const MAX_LANGS = 6; |
| 8 | +const BAR_X = 20, BAR_Y = 42, BAR_W = 760, BAR_H = 22; |
| 9 | + |
| 10 | +async function fetchLangs(owner, token) { |
| 11 | + const headers = { 'User-Agent': 'github-profile-generator', 'Content-Type': 'application/json' }; |
| 12 | + if (token) headers['Authorization'] = `Bearer ${token}`; |
| 13 | + |
| 14 | + const query = `query($login: String!) { |
| 15 | + user(login: $login) { |
| 16 | + repositories(ownerAffiliations: OWNER, isFork: false, first: 100) { |
| 17 | + nodes { |
| 18 | + languages(first: 10, orderBy: { field: SIZE, direction: DESC }) { |
| 19 | + edges { size node { name color } } |
| 20 | + } |
| 21 | + } |
| 22 | + } |
| 23 | + } |
| 24 | + }`; |
| 25 | + |
| 26 | + const res = await fetch('https://api.github.com/graphql', { |
| 27 | + method: 'POST', headers, |
| 28 | + body: JSON.stringify({ query, variables: { login: owner } }), |
| 29 | + }); |
| 30 | + if (!res.ok) throw new Error(`GraphQL HTTP ${res.status}: ${res.statusText}`); |
| 31 | + |
| 32 | + const json = await res.json(); |
| 33 | + if (json.errors?.length) throw new Error(json.errors[0].message); |
| 34 | + |
| 35 | + const totals = new Map(); |
| 36 | + for (const repo of json.data.user.repositories.nodes) { |
| 37 | + for (const { size, node } of repo.languages.edges) { |
| 38 | + const existing = totals.get(node.name); |
| 39 | + if (existing) { |
| 40 | + existing.size += size; |
| 41 | + } else { |
| 42 | + totals.set(node.name, { size, color: node.color || '#808080' }); |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + return [...totals.entries()] |
| 48 | + .map(([name, { size, color }]) => ({ name, size, color })) |
| 49 | + .sort((a, b) => b.size - a.size) |
| 50 | + .slice(0, MAX_LANGS); |
| 51 | +} |
| 52 | + |
| 53 | +function escapeXml(str) { |
| 54 | + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 55 | +} |
| 56 | + |
| 57 | +function buildSvg(langs) { |
| 58 | + const W = 800, H = 120; |
| 59 | + |
| 60 | + if (!langs.length) { |
| 61 | + return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="60" viewBox="0 0 ${W} 60"> |
| 62 | + <style> |
| 63 | + @media (prefers-color-scheme: dark) { .msg { fill: #8b949e; } } |
| 64 | + @media (prefers-color-scheme: light) { .msg { fill: #636e7b; } } |
| 65 | + .msg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 13px; } |
| 66 | + </style> |
| 67 | + <text class="msg" x="${W / 2}" y="35" text-anchor="middle">No language data available.</text> |
| 68 | +</svg>`; |
| 69 | + } |
| 70 | + |
| 71 | + const totalSize = langs.reduce((s, l) => s + l.size, 0); |
| 72 | + const withPct = langs.map(l => ({ ...l, pct: l.size / totalSize })); |
| 73 | + |
| 74 | + // Stacked bar segments |
| 75 | + let barX = BAR_X; |
| 76 | + let barSegs = ''; |
| 77 | + for (const lang of withPct) { |
| 78 | + const segW = Math.round(lang.pct * BAR_W); |
| 79 | + if (segW < 1) continue; |
| 80 | + barSegs += ` <rect x="${barX}" y="${BAR_Y}" width="${segW}" height="${BAR_H}" fill="${lang.color}"/>\n`; |
| 81 | + barX += segW; |
| 82 | + } |
| 83 | + |
| 84 | + // Legend items |
| 85 | + const itemW = Math.floor((W - 40) / langs.length); |
| 86 | + let legend = ''; |
| 87 | + for (let i = 0; i < langs.length; i++) { |
| 88 | + const lx = 20 + i * itemW; |
| 89 | + legend += ` <circle cx="${lx + 5}" cy="88" r="5" fill="${langs[i].color}"/>\n`; |
| 90 | + legend += ` <text x="${lx + 16}" y="93" class="leg">${escapeXml(langs[i].name)} ${(withPct[i].pct * 100).toFixed(1)}%</text>\n`; |
| 91 | + } |
| 92 | + |
| 93 | + return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"> |
| 94 | + <defs> |
| 95 | + <mask id="bar-mask"> |
| 96 | + <rect x="${BAR_X}" y="${BAR_Y}" width="0" height="${BAR_H}" fill="white"> |
| 97 | + <animate attributeName="width" from="0" to="${BAR_W}" dur="1.2s" |
| 98 | + calcMode="spline" keyTimes="0;1" keySplines="0.25 0.1 0.25 1" |
| 99 | + fill="freeze"/> |
| 100 | + </rect> |
| 101 | + </mask> |
| 102 | + <clipPath id="bar-shape"> |
| 103 | + <rect x="${BAR_X}" y="${BAR_Y}" width="${BAR_W}" height="${BAR_H}" rx="4"/> |
| 104 | + </clipPath> |
| 105 | + </defs> |
| 106 | + <style> |
| 107 | + @media (prefers-color-scheme: dark) { |
| 108 | + .ttl { fill: #e6edf3; } |
| 109 | + .trk { fill: #21262d; } |
| 110 | + .leg { fill: #8b949e; } |
| 111 | + } |
| 112 | + @media (prefers-color-scheme: light) { |
| 113 | + .ttl { fill: #1f2328; } |
| 114 | + .trk { fill: #eaeef2; } |
| 115 | + .leg { fill: #636e7b; } |
| 116 | + } |
| 117 | + .ttl { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 600; } |
| 118 | + .leg { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; font-size: 11px; } |
| 119 | + </style> |
| 120 | +
|
| 121 | + <text class="ttl" x="20" y="24">Top Languages</text> |
| 122 | +
|
| 123 | + <!-- Bar track (background) --> |
| 124 | + <rect class="trk" x="${BAR_X}" y="${BAR_Y}" width="${BAR_W}" height="${BAR_H}" rx="4"/> |
| 125 | +
|
| 126 | + <!-- Colored segments: rounded via clipPath, animated via mask --> |
| 127 | + <g clip-path="url(#bar-shape)" mask="url(#bar-mask)"> |
| 128 | +${barSegs} </g> |
| 129 | +
|
| 130 | + <!-- Legend --> |
| 131 | +${legend} |
| 132 | +</svg>`; |
| 133 | +} |
| 134 | + |
| 135 | +async function main() { |
| 136 | + const owner = process.env.GITHUB_REPOSITORY_OWNER; |
| 137 | + if (!owner) { |
| 138 | + console.error('Error: GITHUB_REPOSITORY_OWNER environment variable is not set.'); |
| 139 | + process.exit(1); |
| 140 | + } |
| 141 | + |
| 142 | + const langs = await fetchLangs(owner, process.env.GITHUB_TOKEN); |
| 143 | + writeFileSync(join(__dirname, '..', 'assets', 'langs.svg'), buildSvg(langs), 'utf8'); |
| 144 | + console.log(`Generated assets/langs.svg — ${langs.map(l => l.name).join(', ')}`); |
| 145 | +} |
| 146 | + |
| 147 | +main(); |
0 commit comments