@stats-organization/github-readme-stats-core 2.1.4 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +34 -0
- package/build/api/index.d.ts.map +1 -1
- package/build/api/index.js +18 -0
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +17 -0
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +36 -4
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +34 -0
- package/build/calculateRank.d.ts +12 -11
- package/build/calculateRank.d.ts.map +1 -1
- package/build/calculateRank.js +18 -18
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +29 -20
- package/build/cards/stats.d.ts +4 -2
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +21 -8
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +39 -11
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +29 -8
- package/build/common/Card.d.ts +9 -3
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +47 -19
- package/build/common/color.d.ts +42 -8
- package/build/common/color.d.ts.map +1 -1
- package/build/common/color.js +62 -13
- package/build/common/html.js +1 -1
- package/build/common/http.d.ts +13 -4
- package/build/common/http.d.ts.map +1 -1
- package/build/common/http.js +3 -3
- package/build/common/languageColors.json +9 -1
- package/build/common/ops.d.ts +30 -29
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +32 -37
- package/build/common/render.d.ts +115 -111
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +138 -87
- package/build/common/retryer.d.ts +14 -6
- package/build/common/retryer.d.ts.map +1 -1
- package/build/common/retryer.js +1 -0
- package/build/fetchers/gist.d.ts +6 -21
- package/build/fetchers/gist.d.ts.map +1 -1
- package/build/fetchers/gist.js +30 -35
- package/build/fetchers/repo.js +1 -1
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +0 -1
- package/build/fetchers/types.d.ts +126 -0
- package/build/fetchers/types.d.ts.map +1 -0
- package/build/fetchers/types.js +1 -0
- package/package.json +4 -4
- package/src/_emoji-name-map.d.ts +10 -0
- package/src/api/gist.js +36 -0
- package/src/api/index.js +19 -0
- package/src/api/pin.js +18 -0
- package/src/api/top-langs.js +38 -4
- package/src/api/wakatime.js +36 -0
- package/src/{calculateRank.js → calculateRank.ts} +29 -19
- package/src/cards/repo.js +30 -20
- package/src/cards/stats.js +22 -8
- package/src/cards/top-languages.js +49 -11
- package/src/cards/wakatime.js +33 -8
- package/src/common/Card.ts +51 -16
- package/src/common/color.ts +85 -22
- package/src/common/html.ts +1 -1
- package/src/common/http.ts +31 -0
- package/src/common/languageColors.json +9 -1
- package/src/common/{ops.js → ops.ts} +48 -50
- package/src/common/{render.js → render.ts} +208 -96
- package/src/common/retryer.ts +18 -10
- package/src/fetchers/gist.ts +136 -0
- package/src/fetchers/repo.js +1 -1
- package/src/fetchers/stats.js +0 -1
- package/src/common/http.js +0 -19
- package/src/fetchers/gist.js +0 -112
- /package/src/fetchers/{types.d.ts → types.ts} +0 -0
package/src/cards/stats.js
CHANGED
|
@@ -3,6 +3,7 @@ import { I18n } from "../common/I18n.js";
|
|
|
3
3
|
import { getCardColors } from "../common/color.js";
|
|
4
4
|
import { CustomError } from "../common/error.js";
|
|
5
5
|
import { kFormatter } from "../common/fmt.js";
|
|
6
|
+
import { encodeHTML } from "../common/html.js";
|
|
6
7
|
import { icons, rankIcon } from "../common/icons.js";
|
|
7
8
|
import { buildSearchFilter, clampValue } from "../common/ops.js";
|
|
8
9
|
import { flexLayout, measureText } from "../common/render.js";
|
|
@@ -52,8 +53,10 @@ const LONG_LOCALES = [
|
|
|
52
53
|
/**
|
|
53
54
|
* Create a stats card text item.
|
|
54
55
|
*
|
|
56
|
+
* The caller must ensure that the passed `icon` and `link` are properly sanitized!
|
|
57
|
+
*
|
|
55
58
|
* @param {object} params Object that contains the createTextNode parameters.
|
|
56
|
-
* @param {string} params.icon The icon to display.
|
|
59
|
+
* @param {string} params.icon The sanitized icon to display.
|
|
57
60
|
* @param {string} params.label The label to display.
|
|
58
61
|
* @param {number} params.value The value to display.
|
|
59
62
|
* @param {string} params.id The id of the stat.
|
|
@@ -64,7 +67,7 @@ const LONG_LOCALES = [
|
|
|
64
67
|
* @param {boolean} params.bold Whether to bold the label.
|
|
65
68
|
* @param {string} params.numberFormat The format of numbers on card.
|
|
66
69
|
* @param {number=} params.numberPrecision The precision of numbers on card.
|
|
67
|
-
* @param {string} params.link
|
|
70
|
+
* @param {string} params.link Sanitized url to link to.
|
|
68
71
|
* @param {number} params.labelXOffset horizontal offset for label.
|
|
69
72
|
* @returns {string} The stats card text item SVG object.
|
|
70
73
|
*/
|
|
@@ -83,6 +86,16 @@ const createTextNode = ({
|
|
|
83
86
|
link,
|
|
84
87
|
labelXOffset = 25,
|
|
85
88
|
}) => {
|
|
89
|
+
if (!Number.isFinite(labelXOffset)) {
|
|
90
|
+
throw new Error(`Invalid labelXOffset: "${labelXOffset}"`);
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isFinite(shiftValuePos)) {
|
|
93
|
+
throw new Error(`Invalid shiftValuePos: "${shiftValuePos}"`);
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isFinite(index)) {
|
|
96
|
+
throw new Error(`Invalid index: "${index}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
const precision =
|
|
87
100
|
typeof numberPrecision === "number" && !isNaN(numberPrecision)
|
|
88
101
|
? clampValue(numberPrecision, 0, 2)
|
|
@@ -109,7 +122,7 @@ const createTextNode = ({
|
|
|
109
122
|
${iconSvg}
|
|
110
123
|
<text class="stat ${
|
|
111
124
|
bold ? " bold" : "not_bold"
|
|
112
|
-
}" ${labelOffset} y="12.5">${label}:</text>
|
|
125
|
+
}" ${labelOffset} y="12.5">${encodeHTML(label)}:</text>
|
|
113
126
|
<text
|
|
114
127
|
class="stat ${bold ? " bold" : "not_bold"}"
|
|
115
128
|
x="${(showIcons ? 140 : 120) + (bold ? 5 : 0) + shiftValuePos}"
|
|
@@ -422,13 +435,14 @@ const renderStatsCard = (
|
|
|
422
435
|
}
|
|
423
436
|
|
|
424
437
|
let repoFilter = encodeURIComponent(buildSearchFilter(repo, owner));
|
|
438
|
+
const encodedUsername = encodeURIComponent(username);
|
|
425
439
|
if (show.includes("prs_authored")) {
|
|
426
440
|
STATS.prs_authored = {
|
|
427
441
|
icon: icons.prs,
|
|
428
442
|
label: i18n.t("statcard.prs-authored"),
|
|
429
443
|
value: totalPRsAuthored,
|
|
430
444
|
id: "prs_authored",
|
|
431
|
-
link: `https://github.com/search?q=${repoFilter}author%3A${
|
|
445
|
+
link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&type=pullrequests`,
|
|
432
446
|
};
|
|
433
447
|
}
|
|
434
448
|
if (show.includes("prs_commented")) {
|
|
@@ -437,7 +451,7 @@ const renderStatsCard = (
|
|
|
437
451
|
label: i18n.t("statcard.prs-commented"),
|
|
438
452
|
value: totalPRsCommented,
|
|
439
453
|
id: "prs_commented",
|
|
440
|
-
link: `https://github.com/search?q=${repoFilter}commenter%3A${
|
|
454
|
+
link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&type=pullrequests`,
|
|
441
455
|
};
|
|
442
456
|
}
|
|
443
457
|
if (show.includes("prs_reviewed")) {
|
|
@@ -446,7 +460,7 @@ const renderStatsCard = (
|
|
|
446
460
|
label: i18n.t("statcard.prs-reviewed"),
|
|
447
461
|
value: totalPRsReviewed,
|
|
448
462
|
id: "prs_reviewed",
|
|
449
|
-
link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${
|
|
463
|
+
link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&type=pullrequests`,
|
|
450
464
|
};
|
|
451
465
|
}
|
|
452
466
|
if (show.includes("issues_authored")) {
|
|
@@ -455,7 +469,7 @@ const renderStatsCard = (
|
|
|
455
469
|
label: i18n.t("statcard.issues-authored"),
|
|
456
470
|
value: totalIssuesAuthored,
|
|
457
471
|
id: "issues_authored",
|
|
458
|
-
link: `https://github.com/search?q=${repoFilter}author%3A${
|
|
472
|
+
link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&type=issues`,
|
|
459
473
|
};
|
|
460
474
|
}
|
|
461
475
|
if (show.includes("issues_commented")) {
|
|
@@ -464,7 +478,7 @@ const renderStatsCard = (
|
|
|
464
478
|
label: i18n.t("statcard.issues-commented"),
|
|
465
479
|
value: totalIssuesCommented,
|
|
466
480
|
id: "issues_commented",
|
|
467
|
-
link: `https://github.com/search?q=${repoFilter}commenter%3A${
|
|
481
|
+
link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&type=issues`,
|
|
468
482
|
};
|
|
469
483
|
}
|
|
470
484
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Card } from "../common/Card.js";
|
|
2
2
|
import { I18n } from "../common/I18n.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fallbackColor,
|
|
5
|
+
getCardColors,
|
|
6
|
+
isPrefixedHexColor,
|
|
7
|
+
} from "../common/color.js";
|
|
4
8
|
import { formatBytes } from "../common/fmt.js";
|
|
9
|
+
import { encodeHTML } from "../common/html.js";
|
|
5
10
|
import { chunkArray, clampValue, lowercaseTrim } from "../common/ops.js";
|
|
6
11
|
import {
|
|
7
12
|
createProgressNode,
|
|
@@ -244,8 +249,8 @@ const createProgressTextNode = ({
|
|
|
244
249
|
|
|
245
250
|
return `
|
|
246
251
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
247
|
-
<text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
|
|
248
|
-
${hideValues ? "" : `<text x="${progressTextX}" y="34" class="lang-name">${displayValue}</text>`}
|
|
252
|
+
<text data-testid="lang-name" x="2" y="15" class="lang-name">${encodeHTML(name)}</text>
|
|
253
|
+
${hideValues ? "" : `<text x="${progressTextX}" y="34" class="lang-name">${encodeHTML(displayValue)}</text>`}
|
|
249
254
|
${createProgressNode({
|
|
250
255
|
x: 0,
|
|
251
256
|
y: 25,
|
|
@@ -285,11 +290,15 @@ const createCompactLangNode = ({
|
|
|
285
290
|
const staggerDelay = (index + 3) * 150;
|
|
286
291
|
const color = lang.color || "#858585";
|
|
287
292
|
|
|
293
|
+
if (!isPrefixedHexColor(color)) {
|
|
294
|
+
throw new Error(`Invalid language color: "${color}"`);
|
|
295
|
+
}
|
|
296
|
+
|
|
288
297
|
return `
|
|
289
298
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
|
290
299
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
|
291
300
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
|
292
|
-
${lang.name} ${hideProgress || hideValues ? "" : displayValue}
|
|
301
|
+
${encodeHTML(lang.name)} ${hideProgress || hideValues ? "" : encodeHTML(displayValue)}
|
|
293
302
|
</text>
|
|
294
303
|
</g>
|
|
295
304
|
`;
|
|
@@ -439,6 +448,11 @@ const renderCompactLayout = (
|
|
|
439
448
|
let progressOffset = 0;
|
|
440
449
|
const compactProgressBar = langs
|
|
441
450
|
.map((lang) => {
|
|
451
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
452
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
453
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
454
|
+
}
|
|
455
|
+
|
|
442
456
|
const percentage = parseFloat(
|
|
443
457
|
((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
|
|
444
458
|
);
|
|
@@ -453,7 +467,7 @@ const renderCompactLayout = (
|
|
|
453
467
|
y="0"
|
|
454
468
|
width="${progress}"
|
|
455
469
|
height="8"
|
|
456
|
-
fill="${
|
|
470
|
+
fill="${langColor}"
|
|
457
471
|
/>
|
|
458
472
|
`;
|
|
459
473
|
progressOffset += percentage;
|
|
@@ -514,18 +528,23 @@ const renderDonutVerticalLayout = (
|
|
|
514
528
|
|
|
515
529
|
// Generate each donut vertical chart part
|
|
516
530
|
for (const lang of langs) {
|
|
531
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
532
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
533
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
534
|
+
}
|
|
535
|
+
|
|
517
536
|
const percentage = (lang.size / totalLanguageSize) * 100;
|
|
518
537
|
const circleLength = totalCircleLength * (percentage / 100);
|
|
519
538
|
const delay = startDelayCoefficient * 100;
|
|
520
539
|
|
|
521
540
|
circles.push(`
|
|
522
541
|
<g class="stagger" style="animation-delay: ${delay}ms">
|
|
523
|
-
<circle
|
|
542
|
+
<circle
|
|
524
543
|
cx="150"
|
|
525
544
|
cy="100"
|
|
526
545
|
r="${radius}"
|
|
527
546
|
fill="transparent"
|
|
528
|
-
stroke="${
|
|
547
|
+
stroke="${langColor}"
|
|
529
548
|
stroke-width="25"
|
|
530
549
|
stroke-dasharray="${totalCircleLength}"
|
|
531
550
|
stroke-dashoffset="${indent}"
|
|
@@ -589,6 +608,11 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
589
608
|
|
|
590
609
|
// Generate each pie chart part
|
|
591
610
|
for (const lang of langs) {
|
|
611
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
612
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
613
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
614
|
+
}
|
|
615
|
+
|
|
592
616
|
if (langs.length === 1) {
|
|
593
617
|
paths.push(`
|
|
594
618
|
<circle
|
|
@@ -596,7 +620,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
596
620
|
cy="${centerY}"
|
|
597
621
|
r="${radius}"
|
|
598
622
|
stroke="none"
|
|
599
|
-
fill="${
|
|
623
|
+
fill="${langColor}"
|
|
600
624
|
data-testid="lang-pie"
|
|
601
625
|
size="100"
|
|
602
626
|
/>
|
|
@@ -629,7 +653,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
|
|
|
629
653
|
data-testid="lang-pie"
|
|
630
654
|
size="${percentage}"
|
|
631
655
|
d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
|
|
632
|
-
fill="${
|
|
656
|
+
fill="${langColor}"
|
|
633
657
|
/>
|
|
634
658
|
</g>
|
|
635
659
|
`);
|
|
@@ -716,12 +740,22 @@ const renderDonutLayout = (
|
|
|
716
740
|
statsFormat,
|
|
717
741
|
hideValues,
|
|
718
742
|
) => {
|
|
743
|
+
if (!Number.isFinite(width)) {
|
|
744
|
+
throw new Error(`Invalid width: "${width}"`);
|
|
745
|
+
}
|
|
746
|
+
|
|
719
747
|
const centerX = width / 3;
|
|
720
748
|
const centerY = width / 3;
|
|
721
749
|
const radius = centerX - 60;
|
|
722
750
|
const strokeWidth = 12;
|
|
723
751
|
|
|
724
|
-
const colors = langs.map((lang) =>
|
|
752
|
+
const colors = langs.map((lang) => {
|
|
753
|
+
const langColor = lang.color || DEFAULT_LANG_COLOR;
|
|
754
|
+
if (!isPrefixedHexColor(langColor)) {
|
|
755
|
+
throw new Error(`Invalid language color: "${langColor}"`);
|
|
756
|
+
}
|
|
757
|
+
return langColor;
|
|
758
|
+
});
|
|
725
759
|
const langsPercents = langs.map((lang) =>
|
|
726
760
|
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
|
|
727
761
|
);
|
|
@@ -783,10 +817,14 @@ const renderDonutLayout = (
|
|
|
783
817
|
* @returns {string} No languages data SVG node string.
|
|
784
818
|
*/
|
|
785
819
|
const noLanguagesDataNode = ({ color, text, layout }) => {
|
|
820
|
+
if (!isPrefixedHexColor(color)) {
|
|
821
|
+
throw new Error(`Invalid text color: "${color}"`);
|
|
822
|
+
}
|
|
823
|
+
|
|
786
824
|
return `
|
|
787
825
|
<text x="${
|
|
788
826
|
layout === "pie" || layout === "donut-vertical" ? CARD_PADDING : 0
|
|
789
|
-
}" y="11" class="stat bold" fill="${color}">${text}</text>
|
|
827
|
+
}" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}</text>
|
|
790
828
|
`;
|
|
791
829
|
};
|
|
792
830
|
|
package/src/cards/wakatime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Card } from "../common/Card.js";
|
|
2
2
|
import { I18n } from "../common/I18n.js";
|
|
3
|
-
import { getCardColors } from "../common/color.js";
|
|
3
|
+
import { getCardColors, isPrefixedHexColor } from "../common/color.js";
|
|
4
|
+
import { encodeHTML } from "../common/html.js";
|
|
4
5
|
import languageColors from "../common/languageColors.json" with { type: "json" };
|
|
5
6
|
import { clampValue, lowercaseTrim } from "../common/ops.js";
|
|
6
7
|
import { createProgressNode, flexLayout } from "../common/render.js";
|
|
@@ -24,8 +25,12 @@ const TOTAL_TEXT_WIDTH = 275;
|
|
|
24
25
|
* @returns {string} No coding activity SVG node string.
|
|
25
26
|
*/
|
|
26
27
|
const noCodingActivityNode = ({ color, text }) => {
|
|
28
|
+
if (!isPrefixedHexColor(color)) {
|
|
29
|
+
throw new Error(`Invalid text color: "${color}"`);
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
return `
|
|
28
|
-
<text x="25" y="11" class="stat bold" fill="${color}">${text}</text>
|
|
33
|
+
<text x="25" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}</text>
|
|
29
34
|
`;
|
|
30
35
|
};
|
|
31
36
|
|
|
@@ -58,6 +63,13 @@ const formatLanguageValue = ({ display_format, lang }) => {
|
|
|
58
63
|
* @returns {string} The compact layout language SVG node.
|
|
59
64
|
*/
|
|
60
65
|
const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
|
66
|
+
if (!Number.isFinite(x)) {
|
|
67
|
+
throw new Error(`Invalid x: "${x}"`);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isFinite(y)) {
|
|
70
|
+
throw new Error(`Invalid y: "${y}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
// @ts-ignore
|
|
62
74
|
const color = languageColors[lang.name] || "#858585";
|
|
63
75
|
const value = formatLanguageValue({ display_format, lang });
|
|
@@ -66,7 +78,7 @@ const createCompactLangNode = ({ lang, x, y, display_format }) => {
|
|
|
66
78
|
<g transform="translate(${x}, ${y})">
|
|
67
79
|
<circle cx="5" cy="6" r="5" fill="${color}" />
|
|
68
80
|
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
|
69
|
-
${lang.name} - ${value}
|
|
81
|
+
${encodeHTML(lang.name)} - ${encodeHTML(value)}
|
|
70
82
|
</text>
|
|
71
83
|
</g>
|
|
72
84
|
`;
|
|
@@ -125,6 +137,13 @@ const createTextNode = ({
|
|
|
125
137
|
progressBarBackgroundColor,
|
|
126
138
|
progressBarWidth,
|
|
127
139
|
}) => {
|
|
140
|
+
if (!Number.isFinite(index)) {
|
|
141
|
+
throw new Error(`Invalid index: "${index}"`);
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isFinite(progressBarWidth)) {
|
|
144
|
+
throw new Error(`Invalid progressBarWidth: "${progressBarWidth}"`);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
const staggerDelay = (index + 3) * 150;
|
|
129
148
|
const cardProgress = hideProgress
|
|
130
149
|
? null
|
|
@@ -142,12 +161,12 @@ const createTextNode = ({
|
|
|
142
161
|
|
|
143
162
|
return `
|
|
144
163
|
<g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
|
|
145
|
-
<text class="stat bold" y="12.5" data-testid="${id}">${label}:</text>
|
|
164
|
+
<text class="stat bold" y="12.5" data-testid="${encodeHTML(id)}">${encodeHTML(label)}:</text>
|
|
146
165
|
<text
|
|
147
166
|
class="stat"
|
|
148
167
|
x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
|
|
149
168
|
y="12.5"
|
|
150
|
-
>${value}</text>
|
|
169
|
+
>${encodeHTML(value)}</text>
|
|
151
170
|
${cardProgress}
|
|
152
171
|
</g>
|
|
153
172
|
`;
|
|
@@ -179,11 +198,15 @@ const recalculatePercentages = (languages) => {
|
|
|
179
198
|
* @param {string} colors.textColor The text color.
|
|
180
199
|
* @returns {string} Card CSS styles.
|
|
181
200
|
*/
|
|
182
|
-
const getStyles = ({
|
|
201
|
+
const getStyles = function ({
|
|
183
202
|
// eslint-disable-next-line no-unused-vars
|
|
184
203
|
titleColor,
|
|
185
204
|
textColor,
|
|
186
|
-
})
|
|
205
|
+
}) {
|
|
206
|
+
if (!isPrefixedHexColor(textColor)) {
|
|
207
|
+
throw new Error(`Invalid text color: "${textColor}"`);
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
return `
|
|
188
211
|
.stat {
|
|
189
212
|
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
|
|
@@ -377,7 +400,9 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
|
|
|
377
400
|
progressBarColor: titleColor,
|
|
378
401
|
// @ts-ignore
|
|
379
402
|
progressBarBackgroundColor:
|
|
380
|
-
textColor === titleColor
|
|
403
|
+
textColor === titleColor
|
|
404
|
+
? "#fff0" // transparent
|
|
405
|
+
: textColor,
|
|
381
406
|
hideProgress: hide_progress,
|
|
382
407
|
progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
|
|
383
408
|
});
|
package/src/common/Card.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPrefixedHexColor, isValidGradient } from "./color.js";
|
|
1
2
|
import { encodeHTML } from "./html.js";
|
|
2
3
|
import { flexLayout } from "./render.js";
|
|
3
4
|
|
|
@@ -33,6 +34,8 @@ class Card {
|
|
|
33
34
|
/**
|
|
34
35
|
* Creates a new card instance.
|
|
35
36
|
*
|
|
37
|
+
* The caller must ensure that the passed `titlePrefixIcon` is properly sanitized!
|
|
38
|
+
*
|
|
36
39
|
* @param props Card arguments.
|
|
37
40
|
* @param props.width Card width.
|
|
38
41
|
* @param props.height Card height.
|
|
@@ -40,7 +43,7 @@ class Card {
|
|
|
40
43
|
* @param props.colors Card colors arguments.
|
|
41
44
|
* @param props.customTitle Card custom title.
|
|
42
45
|
* @param props.defaultTitle Card default title.
|
|
43
|
-
* @param props.titlePrefixIcon
|
|
46
|
+
* @param props.titlePrefixIcon Sanitized card title prefix icon.
|
|
44
47
|
*/
|
|
45
48
|
constructor({
|
|
46
49
|
width = 100,
|
|
@@ -65,13 +68,11 @@ class Card {
|
|
|
65
68
|
this.hideBorder = false;
|
|
66
69
|
this.hideTitle = false;
|
|
67
70
|
|
|
68
|
-
this.border_radius = border_radius;
|
|
71
|
+
this.border_radius = parseFloat(String(border_radius));
|
|
69
72
|
|
|
70
73
|
// returns theme based colors with proper overrides and defaults
|
|
71
74
|
this.colors = colors;
|
|
72
|
-
this.title =
|
|
73
|
-
customTitle === undefined ? defaultTitle : customTitle,
|
|
74
|
-
);
|
|
75
|
+
this.title = customTitle === undefined ? defaultTitle : customTitle;
|
|
75
76
|
|
|
76
77
|
this.css = "";
|
|
77
78
|
|
|
@@ -104,7 +105,9 @@ class Card {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
|
-
*
|
|
108
|
+
* The caller must ensure that the passed `css` string is properly sanitized!
|
|
109
|
+
*
|
|
110
|
+
* @param value The sanitized CSS to add to the card.
|
|
108
111
|
*/
|
|
109
112
|
setCSS(value: string): void {
|
|
110
113
|
this.css = value;
|
|
@@ -121,10 +124,13 @@ class Card {
|
|
|
121
124
|
* @param value Whether to hide the title or not.
|
|
122
125
|
*/
|
|
123
126
|
setHideTitle(value: boolean): void {
|
|
124
|
-
this.hideTitle
|
|
125
|
-
if (value) {
|
|
127
|
+
if (value && !this.hideTitle) {
|
|
126
128
|
this.height -= 30;
|
|
127
129
|
}
|
|
130
|
+
if (!value && this.hideTitle) {
|
|
131
|
+
this.height += 30;
|
|
132
|
+
}
|
|
133
|
+
this.hideTitle = value;
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/**
|
|
@@ -144,7 +150,7 @@ class Card {
|
|
|
144
150
|
y="0"
|
|
145
151
|
class="header"
|
|
146
152
|
data-testid="header"
|
|
147
|
-
>${this.title}</text>
|
|
153
|
+
>${encodeHTML(this.title)}</text>
|
|
148
154
|
`;
|
|
149
155
|
|
|
150
156
|
const prefixIcon = `
|
|
@@ -180,10 +186,12 @@ class Card {
|
|
|
180
186
|
if (typeof this.colors.bgColor !== "object") {
|
|
181
187
|
return "";
|
|
182
188
|
}
|
|
189
|
+
if (!isValidGradient(this.colors.bgColor)) {
|
|
190
|
+
throw new Error(`Invalid gradient: ${this.colors.bgColor.join(",")}`);
|
|
191
|
+
}
|
|
183
192
|
|
|
184
193
|
const gradients = this.colors.bgColor.slice(1);
|
|
185
|
-
return
|
|
186
|
-
? `
|
|
194
|
+
return `
|
|
187
195
|
<defs>
|
|
188
196
|
<linearGradient
|
|
189
197
|
id="gradient"
|
|
@@ -198,8 +206,7 @@ class Card {
|
|
|
198
206
|
.join(",")}
|
|
199
207
|
</linearGradient>
|
|
200
208
|
</defs>
|
|
201
|
-
|
|
202
|
-
: "";
|
|
209
|
+
`;
|
|
203
210
|
}
|
|
204
211
|
|
|
205
212
|
/**
|
|
@@ -230,10 +237,38 @@ class Card {
|
|
|
230
237
|
};
|
|
231
238
|
|
|
232
239
|
/**
|
|
233
|
-
*
|
|
240
|
+
* The caller must ensure that the passed `body` string is properly sanitized!
|
|
241
|
+
*
|
|
242
|
+
* @param body The sanitized inner body of the card.
|
|
234
243
|
* @returns The rendered card.
|
|
235
244
|
*/
|
|
236
245
|
render(body: string): string {
|
|
246
|
+
if (!Number.isFinite(this.border_radius)) {
|
|
247
|
+
throw new Error(`Invalid border radius: "${this.border_radius}"`);
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
this.colors.titleColor !== undefined &&
|
|
251
|
+
!isPrefixedHexColor(this.colors.titleColor)
|
|
252
|
+
) {
|
|
253
|
+
throw new Error(`Invalid title color: "${this.colors.titleColor}"`);
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
this.colors.borderColor !== undefined &&
|
|
257
|
+
!isPrefixedHexColor(this.colors.borderColor)
|
|
258
|
+
) {
|
|
259
|
+
throw new Error(`Invalid border color: "${this.colors.borderColor}"`);
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
this.colors.bgColor !== undefined &&
|
|
263
|
+
!(typeof this.colors.bgColor === "object"
|
|
264
|
+
? isValidGradient(this.colors.bgColor)
|
|
265
|
+
: isPrefixedHexColor(this.colors.bgColor))
|
|
266
|
+
) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid background color: ${String(this.colors.bgColor)}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
237
272
|
return `
|
|
238
273
|
<svg
|
|
239
274
|
width="${this.width}"
|
|
@@ -244,8 +279,8 @@ class Card {
|
|
|
244
279
|
role="img"
|
|
245
280
|
aria-labelledby="descId"
|
|
246
281
|
>
|
|
247
|
-
<title id="titleId">${this.a11yTitle}</title>
|
|
248
|
-
<desc id="descId">${this.a11yDesc}</desc>
|
|
282
|
+
<title id="titleId">${encodeHTML(this.a11yTitle)}</title>
|
|
283
|
+
<desc id="descId">${encodeHTML(this.a11yDesc)}</desc>
|
|
249
284
|
<style>
|
|
250
285
|
.header {
|
|
251
286
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
package/src/common/color.ts
CHANGED
|
@@ -1,30 +1,86 @@
|
|
|
1
1
|
import { themes } from "../themes/index.js";
|
|
2
2
|
|
|
3
|
+
/** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
|
|
4
|
+
const HEX_COLOR =
|
|
5
|
+
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$/;
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
|
-
* Checks if a
|
|
8
|
+
* Checks if a value is a bare hex color, i.e. hex digits with no `#` prefix
|
|
9
|
+
* (`"f00"`, `"ffffff"`). This is the form user-supplied color params and
|
|
10
|
+
* gradient stops arrive in.
|
|
5
11
|
*
|
|
6
|
-
* @param
|
|
7
|
-
* @returns True if the
|
|
12
|
+
* @param value Value to check.
|
|
13
|
+
* @returns True if the value is a bare hex color.
|
|
8
14
|
*/
|
|
9
|
-
const
|
|
10
|
-
return
|
|
11
|
-
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
|
|
12
|
-
).test(hexColor);
|
|
15
|
+
const isBareHexColor = (value: unknown): boolean => {
|
|
16
|
+
return typeof value === "string" && HEX_COLOR.test(value);
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
20
|
+
* Checks if a value is a `#`-prefixed hex color (`"#f00"`, `"#ffffff"`). This
|
|
21
|
+
* is the form colors take once resolved by {@link getCardColors}, i.e. right
|
|
22
|
+
* before they are written into the SVG.
|
|
17
23
|
*
|
|
18
|
-
* @param
|
|
19
|
-
* @returns True if the
|
|
24
|
+
* @param value Value to check.
|
|
25
|
+
* @returns True if the value is a `#`-prefixed hex color.
|
|
20
26
|
*/
|
|
21
|
-
const
|
|
27
|
+
const isPrefixedHexColor = (value: unknown): boolean => {
|
|
22
28
|
return (
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
typeof value === "string" &&
|
|
30
|
+
value.startsWith("#") &&
|
|
31
|
+
HEX_COLOR.test(value.slice(1))
|
|
25
32
|
);
|
|
26
33
|
};
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the given parts form a valid gradient: a finite numeric angle
|
|
37
|
+
* followed by at least two bare-hex color stops, e.g. `["90", "f00", "0f0"]`.
|
|
38
|
+
* The angle is written into the SVG `gradientTransform="rotate(...)"`.
|
|
39
|
+
*
|
|
40
|
+
* @param parts Gradient parts: `[angle, ...stops]`.
|
|
41
|
+
* @returns True if the parts form a valid gradient.
|
|
42
|
+
*/
|
|
43
|
+
const isValidGradient = (parts: Array<string>): boolean => {
|
|
44
|
+
const [angle, ...stops] = parts;
|
|
45
|
+
return (
|
|
46
|
+
stops.length >= 2 &&
|
|
47
|
+
angle !== undefined &&
|
|
48
|
+
angle.trim() !== "" &&
|
|
49
|
+
Number.isFinite(Number(angle)) &&
|
|
50
|
+
stops.every(isBareHexColor)
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a string is a valid input for a color or gradient.
|
|
56
|
+
*
|
|
57
|
+
* @param color String to check, may be null or undefined.
|
|
58
|
+
* @returns True if the given string is a valid input.
|
|
59
|
+
*/
|
|
60
|
+
const isValidColorInput = (color: string | null | undefined): boolean => {
|
|
61
|
+
if (color === null || color === undefined) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return isValidGradient(color.split(",")) || isBareHexColor(color);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Iterates over a collection of colors inputs and verifies that each is a valid color or gradient.
|
|
69
|
+
*
|
|
70
|
+
* @param colors Object whose values are checked as valid color inputs.
|
|
71
|
+
* @return The first key where the associated input value is not valid. null if all inputs are valid.
|
|
72
|
+
*/
|
|
73
|
+
const findInvalidColor = (
|
|
74
|
+
colors: Record<string, string | null | undefined>,
|
|
75
|
+
): string | null => {
|
|
76
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
77
|
+
if (!isValidColorInput(value)) {
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
|
|
28
84
|
/**
|
|
29
85
|
* Retrieves a gradient if color has more than one valid hex codes else a single color.
|
|
30
86
|
*
|
|
@@ -41,7 +97,7 @@ const fallbackColor = (
|
|
|
41
97
|
return colors;
|
|
42
98
|
}
|
|
43
99
|
|
|
44
|
-
if (color !== undefined &&
|
|
100
|
+
if (color !== undefined && isBareHexColor(color)) {
|
|
45
101
|
return `#${color}`;
|
|
46
102
|
}
|
|
47
103
|
|
|
@@ -82,13 +138,13 @@ const getCardColors = ({
|
|
|
82
138
|
ring_color,
|
|
83
139
|
theme,
|
|
84
140
|
}: {
|
|
85
|
-
title_color?: string;
|
|
86
|
-
text_color?: string;
|
|
87
|
-
icon_color?: string;
|
|
88
|
-
bg_color?: string;
|
|
89
|
-
border_color?: string;
|
|
90
|
-
ring_color?: string;
|
|
91
|
-
theme?: string;
|
|
141
|
+
title_color?: string | undefined;
|
|
142
|
+
text_color?: string | undefined;
|
|
143
|
+
icon_color?: string | undefined;
|
|
144
|
+
bg_color?: string | undefined;
|
|
145
|
+
border_color?: string | undefined;
|
|
146
|
+
ring_color?: string | undefined;
|
|
147
|
+
theme?: string | undefined;
|
|
92
148
|
}): CardColors => {
|
|
93
149
|
const defaultTheme = themes.default;
|
|
94
150
|
const isThemeProvided = theme !== undefined && theme in themes;
|
|
@@ -146,4 +202,11 @@ const getCardColors = ({
|
|
|
146
202
|
return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
|
|
147
203
|
};
|
|
148
204
|
|
|
149
|
-
export {
|
|
205
|
+
export {
|
|
206
|
+
fallbackColor,
|
|
207
|
+
getCardColors,
|
|
208
|
+
findInvalidColor,
|
|
209
|
+
isValidGradient,
|
|
210
|
+
isBareHexColor,
|
|
211
|
+
isPrefixedHexColor,
|
|
212
|
+
};
|
package/src/common/html.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const encodeHTML = (str: string): string => {
|
|
7
7
|
return (
|
|
8
8
|
str
|
|
9
|
-
.replace(/[\u00A0-\u9999<>&](?!#)/gim, (i: string) => {
|
|
9
|
+
.replace(/[\u00A0-\u9999<>&"'](?!#)/gim, (i: string) => {
|
|
10
10
|
return `&#${i.charCodeAt(0)};`;
|
|
11
11
|
})
|
|
12
12
|
// eslint-disable-next-line no-control-regex
|