681e0720a2c3a001745655a4426a0a3123387c83
[websub-hub] / src / template / badge-svg.js
1 'use strict';
2
3 function escapeXml(s) {
4 if (typeof s === 'number') {
5 return s;
6 } else if (typeof s !== 'string') {
7 return undefined;
8 } else {
9 return s
10 .replace(/&/g, '&')
11 .replace(/</g, '&lt;')
12 .replace(/>/g, '&gt;')
13 .replace(/"/g, '&quot;')
14 .replace(/'/g, '&apos;');
15 }
16 }
17
18
19 const ctxDefaults = {
20 charWidth: 7.2,
21 height: 20,
22 labelColor: '#444',
23 messageColor: '#f73',
24 color: '#fff',
25 fontFamily: 'DejaVu Sans,Verdana,Geneva,sans-serif',
26 };
27
28
29 function fixedRound(n, p = 2) {
30 return Number(n.toFixed(p));
31 }
32
33
34 /**
35 * image/svg+xml;charset=utf-8 formatted badge with subscriber count for a topic
36 * @param {Object} ctx - badge-specific context (not request context)
37 * @param {String} label
38 * @param {String} message
39 * @param {String} accessibleText
40 * @returns {String}
41 */
42 module.exports = (ctx, label, message, accessibleText) => {
43
44 ctx = Object.assign({}, ctxDefaults, ctx, {
45 label,
46 message,
47 accessibleText,
48 });
49 ctx.verticalMargin = fixedRound(ctx.height * 0.69);
50 ctx.labelWidth = fixedRound(ctx.label.length * ctx.charWidth);
51 ctx.messageWidth = fixedRound(ctx.message.length * ctx.charWidth);
52 ctx.width = ctx.labelWidth + ctx.messageWidth;
53 ctx.halfCharWidth = fixedRound(ctx.charWidth * 0.5);
54
55 /*
56 * This SVG content mostly replicates the output of the 'Plastic' badge
57 * renderer from https://github.com/badges/shields/tree/master/badge-maker which
58 * is under the http://creativecommons.org/publicdomain/zero/1.0/ license.
59 */
60 return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${ctx.width}" height="${ctx.height}" role="img" aria-label="${escapeXml(ctx.accessibleText)}">
61 <title>${escapeXml(ctx.accessibleText)}</title>
62 <linearGradient id="s" x2="0" y2="100%">
63 <stop offset="0" stop-color="#fff" stop-opacity=".7"/>
64 <stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
65 <stop offset=".9" stop-color="#000" stop-opacity=".3"/>
66 <stop offset="1" stop-color="#000" stop-opacity=".5"/>
67 </linearGradient>
68 <clipPath id="r">
69 <rect width="${ctx.width}" height="${ctx.height}" rx="4" fill="#fff"/>
70 </clipPath>
71 <g clip-path="url(#r)">
72 <rect width="${ctx.labelWidth}" height="${ctx.height}" fill="${ctx.labelColor}"/>
73 <rect x="${ctx.labelWidth}" width="${ctx.messageWidth}" height="${ctx.height}" fill="${ctx.messageColor}"/>
74 <rect width="${ctx.width}" height="${ctx.height}" fill="url(#s)"/>
75 </g>
76 <g fill="${ctx.color}" text-anchor="left" font-family="${ctx.fontFamily}" text-rendering="geometricPrecision" font-size="11" font-weight="bold">
77 <text x="${ctx.halfCharWidth}" y="${ctx.verticalMargin}">${escapeXml(ctx.label)}</text>
78 <text x="${fixedRound(ctx.halfCharWidth + ctx.labelWidth)}" y="${ctx.verticalMargin}">${escapeXml(ctx.message)}</text>
79 </g>
80 </svg>`;
81 };