Skip to content

Commit bc65568

Browse files
fix(svg): add accessible title and desc tags
Added semantic title and desc tags to improve SVG accessibility for screen readers. Escape XML characters in user title and labels Thanks for pointing this out! I’ve added XML escaping for user-controlled strings using an escapeXML() helper and updated the affected <title>, <desc>, and SVG text usages in both SVG generators to prevent malformed SVG/XML injection issues. Implement escapeXML function for XML escaping Add escapeXML function to sanitize XML strings fix(svg): add accessible title/desc tags and escape XML - Added semantic title and desc tags for accessibility - Implemented escapeXML helper to sanitize user strings - Escaped XML characters in user-controlled labels
1 parent 3d5001b commit bc65568

1 file changed

Lines changed: 36 additions & 5 deletions

File tree

lib/svg/generator.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ function deterministicRandom(seed: string): number {
1515
}
1616
return (hash >>> 0) / 4294967296;
1717
}
18-
18+
function escapeXML(str: string): string {
19+
return str
20+
.replace(/&/g, '&amp;')
21+
.replace(/</g, '&lt;')
22+
.replace(/>/g, '&gt;')
23+
.replace(/"/g, '&quot;')
24+
.replace(/'/g, '&#39;');
25+
}
1926
function generateParticles(
2027
x: number,
2128
y: number,
@@ -148,6 +155,7 @@ export function generateSVG(
148155
if (params.autoTheme) {
149156
return generateAutoThemeSVG(stats, params, calendar);
150157
}
158+
const safeUser = escapeXML(params.user || 'GitHub User');
151159

152160
const bg = `#${(params.bg || '0d1117').replace('#', '')}`;
153161
const accent = `#${(params.accent || '00ffaa').replace('#', '')}`;
@@ -216,7 +224,18 @@ export function generateSVG(
216224
: '';
217225

218226
return `
219-
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="420" viewBox="0 0 600 420" fill="none">
227+
<svg
228+
xmlns="http://www.w3.org/2000/svg"
229+
width="600"
230+
height="420"
231+
viewBox="0 0 600 420"
232+
fill="none"
233+
role="img"
234+
>
235+
<title>CommitPulse Stats for ${safeUser}</title>
236+
<desc>
237+
${params.user || 'This user'} has ${stats.totalContributions} total contributions and a longest streak of ${stats.longestStreak} days.
238+
</desc>
220239
<defs>
221240
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
222241
<feGaussianBlur stdDeviation="5" result="blur" />
@@ -287,7 +306,7 @@ export function generateSVG(
287306
<text y="40" class="stats">${stats.longestStreak}</text>
288307
</g>
289308
290-
<text x="300" y="50" text-anchor="middle" class="title">${params.user?.toUpperCase() || ''}</text>
309+
<text x="300" y="50" text-anchor="middle" class="title">${safeUser.toUpperCase()}</text>
291310
292311
<rect x="100" y="60" width="400" height="1" fill="${accent}" fill-opacity="0.3">
293312
<animate attributeName="y" values="80;320;80" dur="${params.speed || '8s'}" repeatCount="indefinite" />
@@ -313,6 +332,7 @@ function generateAutoThemeSVG(
313332
): string {
314333
const light = AUTO_LIGHT_THEME;
315334
const dark = AUTO_DARK_THEME;
335+
const safeUser = escapeXML(params.user || 'GitHub User');
316336

317337
const selectedFont = params.font
318338
? FONT_MAP[params.font.toLowerCase()] || '"JetBrains Mono", monospace'
@@ -363,7 +383,18 @@ function generateAutoThemeSVG(
363383
}
364384

365385
return `
366-
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="420" viewBox="0 0 600 420" fill="none">
386+
<svg
387+
xmlns="http://www.w3.org/2000/svg"
388+
width="600"
389+
height="420"
390+
viewBox="0 0 600 420"
391+
fill="none"
392+
role="img"
393+
>
394+
<title>CommitPulse Stats for ${safeUser} </title>
395+
<desc>
396+
${params.user || 'This user'} has ${stats.totalContributions} total contributions and a longest streak of ${stats.longestStreak} days.
397+
</desc>
367398
<defs>
368399
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
369400
<feGaussianBlur stdDeviation="5" result="blur" />
@@ -455,7 +486,7 @@ function generateAutoThemeSVG(
455486
<text y="40" class="stats">${stats.longestStreak}</text>
456487
</g>
457488
458-
<text x="300" y="50" text-anchor="middle" class="title">${params.user?.toUpperCase() || ''}</text>
489+
<text x="300" y="50" text-anchor="middle" class="title">${safeUser.toUpperCase()}</text>
459490
460491
<rect x="100" y="60" width="400" height="1" class="cp-accent-fill" fill-opacity="0.3">
461492
<animate attributeName="y" values="80;320;80" dur="${params.speed || '8s'}" repeatCount="indefinite" />

0 commit comments

Comments
 (0)