@stats-organization/github-readme-stats-core 2.1.3 → 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.
Files changed (123) hide show
  1. package/build/api/gist.d.ts.map +1 -1
  2. package/build/api/gist.js +34 -1
  3. package/build/api/index.d.ts.map +1 -1
  4. package/build/api/index.js +18 -1
  5. package/build/api/pin.d.ts.map +1 -1
  6. package/build/api/pin.js +17 -1
  7. package/build/api/top-langs.d.ts.map +1 -1
  8. package/build/api/top-langs.js +36 -5
  9. package/build/api/wakatime.d.ts.map +1 -1
  10. package/build/api/wakatime.js +34 -1
  11. package/build/calculateRank.d.ts +12 -11
  12. package/build/calculateRank.d.ts.map +1 -1
  13. package/build/calculateRank.js +18 -18
  14. package/build/cards/gist.d.ts.map +1 -1
  15. package/build/cards/gist.js +0 -1
  16. package/build/cards/repo.d.ts.map +1 -1
  17. package/build/cards/repo.js +29 -21
  18. package/build/cards/stats.d.ts +4 -2
  19. package/build/cards/stats.d.ts.map +1 -1
  20. package/build/cards/stats.js +21 -9
  21. package/build/cards/top-languages.d.ts.map +1 -1
  22. package/build/cards/top-languages.js +39 -12
  23. package/build/cards/wakatime.d.ts.map +1 -1
  24. package/build/cards/wakatime.js +29 -9
  25. package/build/common/Card.d.ts +54 -60
  26. package/build/common/Card.d.ts.map +1 -1
  27. package/build/common/Card.js +88 -61
  28. package/build/common/I18n.d.ts +13 -14
  29. package/build/common/I18n.d.ts.map +1 -1
  30. package/build/common/I18n.js +13 -12
  31. package/build/common/color.d.ts +56 -32
  32. package/build/common/color.d.ts.map +1 -1
  33. package/build/common/color.js +87 -50
  34. package/build/common/config.d.ts +18 -2
  35. package/build/common/config.d.ts.map +1 -1
  36. package/build/common/config.js +17 -26
  37. package/build/common/constants.d.ts +2 -2
  38. package/build/common/constants.d.ts.map +1 -1
  39. package/build/common/constants.js +5 -3
  40. package/build/common/error.d.ts +36 -29
  41. package/build/common/error.d.ts.map +1 -1
  42. package/build/common/error.js +14 -10
  43. package/build/common/fmt.d.ts +14 -13
  44. package/build/common/fmt.d.ts.map +1 -1
  45. package/build/common/fmt.js +19 -15
  46. package/build/common/html.d.ts +2 -4
  47. package/build/common/html.d.ts.map +1 -1
  48. package/build/common/html.js +2 -6
  49. package/build/common/http.d.ts +13 -4
  50. package/build/common/http.d.ts.map +1 -1
  51. package/build/common/http.js +3 -4
  52. package/build/common/icons.d.ts +22 -21
  53. package/build/common/icons.d.ts.map +1 -1
  54. package/build/common/icons.js +4 -5
  55. package/build/common/languageColors.json +9 -1
  56. package/build/common/ops.d.ts +30 -29
  57. package/build/common/ops.d.ts.map +1 -1
  58. package/build/common/ops.js +32 -38
  59. package/build/common/render.d.ts +115 -111
  60. package/build/common/render.d.ts.map +1 -1
  61. package/build/common/render.js +138 -88
  62. package/build/common/retryer.d.ts +24 -13
  63. package/build/common/retryer.d.ts.map +1 -1
  64. package/build/common/retryer.js +22 -30
  65. package/build/fetchers/gist.d.ts +6 -21
  66. package/build/fetchers/gist.d.ts.map +1 -1
  67. package/build/fetchers/gist.js +30 -36
  68. package/build/fetchers/repo.d.ts.map +1 -1
  69. package/build/fetchers/repo.js +4 -7
  70. package/build/fetchers/stats.d.ts.map +1 -1
  71. package/build/fetchers/stats.js +3 -7
  72. package/build/fetchers/top-languages.d.ts.map +1 -1
  73. package/build/fetchers/top-languages.js +0 -1
  74. package/build/fetchers/types.d.ts +126 -0
  75. package/build/fetchers/types.d.ts.map +1 -0
  76. package/build/fetchers/types.js +1 -0
  77. package/build/fetchers/wakatime.d.ts.map +1 -1
  78. package/build/fetchers/wakatime.js +0 -1
  79. package/build/translations.d.ts +627 -24
  80. package/build/translations.d.ts.map +1 -1
  81. package/build/translations.js +7 -8
  82. package/package.json +4 -4
  83. package/src/_emoji-name-map.d.ts +10 -0
  84. package/src/api/gist.js +36 -2
  85. package/src/api/index.js +19 -2
  86. package/src/api/pin.js +18 -2
  87. package/src/api/top-langs.js +38 -6
  88. package/src/api/wakatime.js +36 -2
  89. package/src/{calculateRank.js → calculateRank.ts} +29 -19
  90. package/src/cards/gist.js +0 -2
  91. package/src/cards/repo.js +30 -22
  92. package/src/cards/stats.js +22 -10
  93. package/src/cards/top-languages.js +49 -13
  94. package/src/cards/wakatime.js +33 -10
  95. package/src/common/Card.ts +338 -0
  96. package/src/common/I18n.ts +49 -0
  97. package/src/common/color.ts +212 -0
  98. package/src/common/config.ts +83 -0
  99. package/src/common/constants.ts +10 -0
  100. package/src/common/{error.js → error.ts} +20 -14
  101. package/src/common/{fmt.js → fmt.ts} +27 -19
  102. package/src/common/{html.js → html.ts} +3 -8
  103. package/src/common/http.ts +31 -0
  104. package/src/common/{icons.js → icons.ts} +9 -7
  105. package/src/common/languageColors.json +9 -1
  106. package/src/common/{ops.js → ops.ts} +48 -52
  107. package/src/common/{render.js → render.ts} +208 -98
  108. package/src/common/retryer.ts +125 -0
  109. package/src/fetchers/gist.ts +136 -0
  110. package/src/fetchers/repo.js +4 -8
  111. package/src/fetchers/stats.js +3 -8
  112. package/src/fetchers/top-languages.js +0 -2
  113. package/src/fetchers/{types.d.ts → types.ts} +1 -1
  114. package/src/fetchers/wakatime.js +0 -2
  115. package/src/{translations.js → translations.ts} +14 -10
  116. package/src/common/Card.js +0 -275
  117. package/src/common/I18n.js +0 -42
  118. package/src/common/color.js +0 -145
  119. package/src/common/config.js +0 -78
  120. package/src/common/constants.js +0 -9
  121. package/src/common/http.js +0 -21
  122. package/src/common/retryer.js +0 -104
  123. package/src/fetchers/gist.js +0 -114
@@ -1,9 +1,12 @@
1
- // @ts-check
2
-
3
1
  import { Card } from "../common/Card.js";
4
2
  import { I18n } from "../common/I18n.js";
5
- import { fallbackColor, getCardColors } from "../common/color.js";
3
+ import {
4
+ fallbackColor,
5
+ getCardColors,
6
+ isPrefixedHexColor,
7
+ } from "../common/color.js";
6
8
  import { formatBytes } from "../common/fmt.js";
9
+ import { encodeHTML } from "../common/html.js";
7
10
  import { chunkArray, clampValue, lowercaseTrim } from "../common/ops.js";
8
11
  import {
9
12
  createProgressNode,
@@ -246,8 +249,8 @@ const createProgressTextNode = ({
246
249
 
247
250
  return `
248
251
  <g class="stagger" style="animation-delay: ${staggerDelay}ms">
249
- <text data-testid="lang-name" x="2" y="15" class="lang-name">${name}</text>
250
- ${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>`}
251
254
  ${createProgressNode({
252
255
  x: 0,
253
256
  y: 25,
@@ -287,11 +290,15 @@ const createCompactLangNode = ({
287
290
  const staggerDelay = (index + 3) * 150;
288
291
  const color = lang.color || "#858585";
289
292
 
293
+ if (!isPrefixedHexColor(color)) {
294
+ throw new Error(`Invalid language color: "${color}"`);
295
+ }
296
+
290
297
  return `
291
298
  <g class="stagger" style="animation-delay: ${staggerDelay}ms">
292
299
  <circle cx="5" cy="6" r="5" fill="${color}" />
293
300
  <text data-testid="lang-name" x="15" y="10" class='lang-name'>
294
- ${lang.name} ${hideProgress || hideValues ? "" : displayValue}
301
+ ${encodeHTML(lang.name)} ${hideProgress || hideValues ? "" : encodeHTML(displayValue)}
295
302
  </text>
296
303
  </g>
297
304
  `;
@@ -441,6 +448,11 @@ const renderCompactLayout = (
441
448
  let progressOffset = 0;
442
449
  const compactProgressBar = langs
443
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
+
444
456
  const percentage = parseFloat(
445
457
  ((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
446
458
  );
@@ -455,7 +467,7 @@ const renderCompactLayout = (
455
467
  y="0"
456
468
  width="${progress}"
457
469
  height="8"
458
- fill="${lang.color || "#858585"}"
470
+ fill="${langColor}"
459
471
  />
460
472
  `;
461
473
  progressOffset += percentage;
@@ -516,18 +528,23 @@ const renderDonutVerticalLayout = (
516
528
 
517
529
  // Generate each donut vertical chart part
518
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
+
519
536
  const percentage = (lang.size / totalLanguageSize) * 100;
520
537
  const circleLength = totalCircleLength * (percentage / 100);
521
538
  const delay = startDelayCoefficient * 100;
522
539
 
523
540
  circles.push(`
524
541
  <g class="stagger" style="animation-delay: ${delay}ms">
525
- <circle
542
+ <circle
526
543
  cx="150"
527
544
  cy="100"
528
545
  r="${radius}"
529
546
  fill="transparent"
530
- stroke="${lang.color}"
547
+ stroke="${langColor}"
531
548
  stroke-width="25"
532
549
  stroke-dasharray="${totalCircleLength}"
533
550
  stroke-dashoffset="${indent}"
@@ -591,6 +608,11 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
591
608
 
592
609
  // Generate each pie chart part
593
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
+
594
616
  if (langs.length === 1) {
595
617
  paths.push(`
596
618
  <circle
@@ -598,7 +620,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
598
620
  cy="${centerY}"
599
621
  r="${radius}"
600
622
  stroke="none"
601
- fill="${lang.color}"
623
+ fill="${langColor}"
602
624
  data-testid="lang-pie"
603
625
  size="100"
604
626
  />
@@ -631,7 +653,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat, hideValues) => {
631
653
  data-testid="lang-pie"
632
654
  size="${percentage}"
633
655
  d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
634
- fill="${lang.color}"
656
+ fill="${langColor}"
635
657
  />
636
658
  </g>
637
659
  `);
@@ -718,12 +740,22 @@ const renderDonutLayout = (
718
740
  statsFormat,
719
741
  hideValues,
720
742
  ) => {
743
+ if (!Number.isFinite(width)) {
744
+ throw new Error(`Invalid width: "${width}"`);
745
+ }
746
+
721
747
  const centerX = width / 3;
722
748
  const centerY = width / 3;
723
749
  const radius = centerX - 60;
724
750
  const strokeWidth = 12;
725
751
 
726
- const colors = langs.map((lang) => lang.color);
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
+ });
727
759
  const langsPercents = langs.map((lang) =>
728
760
  parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
729
761
  );
@@ -785,10 +817,14 @@ const renderDonutLayout = (
785
817
  * @returns {string} No languages data SVG node string.
786
818
  */
787
819
  const noLanguagesDataNode = ({ color, text, layout }) => {
820
+ if (!isPrefixedHexColor(color)) {
821
+ throw new Error(`Invalid text color: "${color}"`);
822
+ }
823
+
788
824
  return `
789
825
  <text x="${
790
826
  layout === "pie" || layout === "donut-vertical" ? CARD_PADDING : 0
791
- }" y="11" class="stat bold" fill="${color}">${text}</text>
827
+ }" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}</text>
792
828
  `;
793
829
  };
794
830
 
@@ -1,8 +1,7 @@
1
- // @ts-check
2
-
3
1
  import { Card } from "../common/Card.js";
4
2
  import { I18n } from "../common/I18n.js";
5
- import { getCardColors } from "../common/color.js";
3
+ import { getCardColors, isPrefixedHexColor } from "../common/color.js";
4
+ import { encodeHTML } from "../common/html.js";
6
5
  import languageColors from "../common/languageColors.json" with { type: "json" };
7
6
  import { clampValue, lowercaseTrim } from "../common/ops.js";
8
7
  import { createProgressNode, flexLayout } from "../common/render.js";
@@ -26,8 +25,12 @@ const TOTAL_TEXT_WIDTH = 275;
26
25
  * @returns {string} No coding activity SVG node string.
27
26
  */
28
27
  const noCodingActivityNode = ({ color, text }) => {
28
+ if (!isPrefixedHexColor(color)) {
29
+ throw new Error(`Invalid text color: "${color}"`);
30
+ }
31
+
29
32
  return `
30
- <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>
31
34
  `;
32
35
  };
33
36
 
@@ -60,6 +63,13 @@ const formatLanguageValue = ({ display_format, lang }) => {
60
63
  * @returns {string} The compact layout language SVG node.
61
64
  */
62
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
+
63
73
  // @ts-ignore
64
74
  const color = languageColors[lang.name] || "#858585";
65
75
  const value = formatLanguageValue({ display_format, lang });
@@ -68,7 +78,7 @@ const createCompactLangNode = ({ lang, x, y, display_format }) => {
68
78
  <g transform="translate(${x}, ${y})">
69
79
  <circle cx="5" cy="6" r="5" fill="${color}" />
70
80
  <text data-testid="lang-name" x="15" y="10" class='lang-name'>
71
- ${lang.name} - ${value}
81
+ ${encodeHTML(lang.name)} - ${encodeHTML(value)}
72
82
  </text>
73
83
  </g>
74
84
  `;
@@ -127,6 +137,13 @@ const createTextNode = ({
127
137
  progressBarBackgroundColor,
128
138
  progressBarWidth,
129
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
+
130
147
  const staggerDelay = (index + 3) * 150;
131
148
  const cardProgress = hideProgress
132
149
  ? null
@@ -144,12 +161,12 @@ const createTextNode = ({
144
161
 
145
162
  return `
146
163
  <g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">
147
- <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>
148
165
  <text
149
166
  class="stat"
150
167
  x="${hideProgress ? HIDDEN_PROGRESSBAR_PADDING : PROGRESSBAR_PADDING + progressBarWidth}"
151
168
  y="12.5"
152
- >${value}</text>
169
+ >${encodeHTML(value)}</text>
153
170
  ${cardProgress}
154
171
  </g>
155
172
  `;
@@ -181,11 +198,15 @@ const recalculatePercentages = (languages) => {
181
198
  * @param {string} colors.textColor The text color.
182
199
  * @returns {string} Card CSS styles.
183
200
  */
184
- const getStyles = ({
201
+ const getStyles = function ({
185
202
  // eslint-disable-next-line no-unused-vars
186
203
  titleColor,
187
204
  textColor,
188
- }) => {
205
+ }) {
206
+ if (!isPrefixedHexColor(textColor)) {
207
+ throw new Error(`Invalid text color: "${textColor}"`);
208
+ }
209
+
189
210
  return `
190
211
  .stat {
191
212
  font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
@@ -379,7 +400,9 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => {
379
400
  progressBarColor: titleColor,
380
401
  // @ts-ignore
381
402
  progressBarBackgroundColor:
382
- textColor === titleColor ? bgColor : textColor,
403
+ textColor === titleColor
404
+ ? "#fff0" // transparent
405
+ : textColor,
383
406
  hideProgress: hide_progress,
384
407
  progressBarWidth: normalizedWidth - TOTAL_TEXT_WIDTH,
385
408
  });
@@ -0,0 +1,338 @@
1
+ import { isPrefixedHexColor, isValidGradient } from "./color.js";
2
+ import { encodeHTML } from "./html.js";
3
+ import { flexLayout } from "./render.js";
4
+
5
+ interface CardColors {
6
+ /** Card title color. */
7
+ titleColor?: string;
8
+ /** Card text color. */
9
+ textColor?: string;
10
+ /** Card icon color. */
11
+ iconColor?: string;
12
+ /** Card background color. */
13
+ bgColor?: string | Array<string>;
14
+ /** Card border color. */
15
+ borderColor?: string;
16
+ }
17
+
18
+ class Card {
19
+ width: number;
20
+ height: number;
21
+ hideBorder: boolean;
22
+ hideTitle: boolean;
23
+ border_radius: number;
24
+ colors: CardColors;
25
+ title: string;
26
+ css: string;
27
+ paddingX: number;
28
+ paddingY: number;
29
+ titlePrefixIcon: string | undefined;
30
+ animations: boolean;
31
+ a11yTitle: string;
32
+ a11yDesc: string;
33
+
34
+ /**
35
+ * Creates a new card instance.
36
+ *
37
+ * The caller must ensure that the passed `titlePrefixIcon` is properly sanitized!
38
+ *
39
+ * @param props Card arguments.
40
+ * @param props.width Card width.
41
+ * @param props.height Card height.
42
+ * @param props.border_radius Card border radius.
43
+ * @param props.colors Card colors arguments.
44
+ * @param props.customTitle Card custom title.
45
+ * @param props.defaultTitle Card default title.
46
+ * @param props.titlePrefixIcon Sanitized card title prefix icon.
47
+ */
48
+ constructor({
49
+ width = 100,
50
+ height = 100,
51
+ border_radius = 4.5,
52
+ colors = {},
53
+ customTitle,
54
+ defaultTitle = "",
55
+ titlePrefixIcon,
56
+ }: {
57
+ width?: number;
58
+ height?: number;
59
+ border_radius?: number;
60
+ colors?: CardColors;
61
+ customTitle?: string;
62
+ defaultTitle?: string;
63
+ titlePrefixIcon?: string;
64
+ }) {
65
+ this.width = width;
66
+ this.height = height;
67
+
68
+ this.hideBorder = false;
69
+ this.hideTitle = false;
70
+
71
+ this.border_radius = parseFloat(String(border_radius));
72
+
73
+ // returns theme based colors with proper overrides and defaults
74
+ this.colors = colors;
75
+ this.title = customTitle === undefined ? defaultTitle : customTitle;
76
+
77
+ this.css = "";
78
+
79
+ this.paddingX = 25;
80
+ this.paddingY = 35;
81
+ this.titlePrefixIcon = titlePrefixIcon;
82
+ this.animations = true;
83
+ this.a11yTitle = "";
84
+ this.a11yDesc = "";
85
+ }
86
+
87
+ disableAnimations(): void {
88
+ this.animations = false;
89
+ }
90
+
91
+ /**
92
+ * @param props The props object.
93
+ * @param props.title Accessibility title.
94
+ * @param props.desc Accessibility description.
95
+ */
96
+ setAccessibilityLabel({
97
+ title,
98
+ desc,
99
+ }: {
100
+ title: string;
101
+ desc: string;
102
+ }): void {
103
+ this.a11yTitle = title;
104
+ this.a11yDesc = desc;
105
+ }
106
+
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.
111
+ */
112
+ setCSS(value: string): void {
113
+ this.css = value;
114
+ }
115
+
116
+ /**
117
+ * @param value Whether to hide the border or not.
118
+ */
119
+ setHideBorder(value: boolean): void {
120
+ this.hideBorder = value;
121
+ }
122
+
123
+ /**
124
+ * @param value Whether to hide the title or not.
125
+ */
126
+ setHideTitle(value: boolean): void {
127
+ if (value && !this.hideTitle) {
128
+ this.height -= 30;
129
+ }
130
+ if (!value && this.hideTitle) {
131
+ this.height += 30;
132
+ }
133
+ this.hideTitle = value;
134
+ }
135
+
136
+ /**
137
+ * @param text The title to set.
138
+ */
139
+ setTitle(text: string): void {
140
+ this.title = text;
141
+ }
142
+
143
+ /**
144
+ * @returns The rendered card title.
145
+ */
146
+ renderTitle(): string {
147
+ const titleText = `
148
+ <text
149
+ x="0"
150
+ y="0"
151
+ class="header"
152
+ data-testid="header"
153
+ >${encodeHTML(this.title)}</text>
154
+ `;
155
+
156
+ const prefixIcon = `
157
+ <svg
158
+ class="icon"
159
+ x="0"
160
+ y="-13"
161
+ viewBox="0 0 16 16"
162
+ version="1.1"
163
+ width="16"
164
+ height="16"
165
+ >
166
+ ${String(this.titlePrefixIcon)}
167
+ </svg>
168
+ `;
169
+ return `
170
+ <g
171
+ data-testid="card-title"
172
+ transform="translate(${this.paddingX}, ${this.paddingY})"
173
+ >
174
+ ${flexLayout({
175
+ items: [this.titlePrefixIcon ? prefixIcon : "", titleText],
176
+ gap: 25,
177
+ }).join("")}
178
+ </g>
179
+ `;
180
+ }
181
+
182
+ /**
183
+ * @returns The rendered card gradient.
184
+ */
185
+ renderGradient(): string {
186
+ if (typeof this.colors.bgColor !== "object") {
187
+ return "";
188
+ }
189
+ if (!isValidGradient(this.colors.bgColor)) {
190
+ throw new Error(`Invalid gradient: ${this.colors.bgColor.join(",")}`);
191
+ }
192
+
193
+ const gradients = this.colors.bgColor.slice(1);
194
+ return `
195
+ <defs>
196
+ <linearGradient
197
+ id="gradient"
198
+ gradientTransform="rotate(${String(this.colors.bgColor[0])})"
199
+ gradientUnits="userSpaceOnUse"
200
+ >
201
+ ${gradients
202
+ .map((grad, index) => {
203
+ const offset = (index * 100) / (gradients.length - 1);
204
+ return `<stop offset="${offset}%" stop-color="#${grad}" />`;
205
+ })
206
+ .join(",")}
207
+ </linearGradient>
208
+ </defs>
209
+ `;
210
+ }
211
+
212
+ /**
213
+ * Retrieves css animations for a card.
214
+ *
215
+ * @returns Animation css.
216
+ */
217
+ getAnimations = (): string => {
218
+ return `
219
+ /* Animations */
220
+ @keyframes scaleInAnimation {
221
+ from {
222
+ transform: translate(-5px, 5px) scale(0);
223
+ }
224
+ to {
225
+ transform: translate(-5px, 5px) scale(1);
226
+ }
227
+ }
228
+ @keyframes fadeInAnimation {
229
+ from {
230
+ opacity: 0;
231
+ }
232
+ to {
233
+ opacity: 1;
234
+ }
235
+ }
236
+ `;
237
+ };
238
+
239
+ /**
240
+ * The caller must ensure that the passed `body` string is properly sanitized!
241
+ *
242
+ * @param body The sanitized inner body of the card.
243
+ * @returns The rendered card.
244
+ */
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
+
272
+ return `
273
+ <svg
274
+ width="${this.width}"
275
+ height="${this.height}"
276
+ viewBox="0 0 ${this.width} ${this.height}"
277
+ fill="none"
278
+ xmlns="http://www.w3.org/2000/svg"
279
+ role="img"
280
+ aria-labelledby="descId"
281
+ >
282
+ <title id="titleId">${encodeHTML(this.a11yTitle)}</title>
283
+ <desc id="descId">${encodeHTML(this.a11yDesc)}</desc>
284
+ <style>
285
+ .header {
286
+ font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
287
+ fill: ${String(this.colors.titleColor)};
288
+ animation: fadeInAnimation 0.8s ease-in-out forwards;
289
+ }
290
+ @supports(-moz-appearance: auto) {
291
+ /* Selector detects Firefox */
292
+ .header { font-size: 15.5px; }
293
+ }
294
+ ${this.css}
295
+
296
+ ${this.getAnimations()}
297
+ ${
298
+ this.animations
299
+ ? ""
300
+ : `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
301
+ }
302
+ </style>
303
+
304
+ ${this.renderGradient()}
305
+
306
+ <rect
307
+ data-testid="card-bg"
308
+ x="0.5"
309
+ y="0.5"
310
+ rx="${this.border_radius}"
311
+ height="99%"
312
+ stroke="${String(this.colors.borderColor)}"
313
+ width="${this.width - 1}"
314
+ fill="${
315
+ typeof this.colors.bgColor === "object"
316
+ ? "url(#gradient)"
317
+ : String(this.colors.bgColor)
318
+ }"
319
+ stroke-opacity="${this.hideBorder ? 0 : 1}"
320
+ />
321
+
322
+ ${this.hideTitle ? "" : this.renderTitle()}
323
+
324
+ <g
325
+ data-testid="main-card-body"
326
+ transform="translate(0, ${
327
+ this.hideTitle ? this.paddingX : this.paddingY + 20
328
+ })"
329
+ >
330
+ ${body}
331
+ </g>
332
+ </svg>
333
+ `;
334
+ }
335
+ }
336
+
337
+ export { Card };
338
+ export default Card;
@@ -0,0 +1,49 @@
1
+ const FALLBACK_LOCALE = "en";
2
+
3
+ /**
4
+ * I18n translation class.
5
+ */
6
+ class I18n<Translations extends Record<string, Record<string, string>>> {
7
+ locale: string;
8
+ translations: Translations;
9
+
10
+ /**
11
+ * @param props Constructor arguments.
12
+ * @param props.locale Locale.
13
+ * @param props.translations Translations.
14
+ */
15
+ constructor({
16
+ locale,
17
+ translations,
18
+ }: {
19
+ locale?: string;
20
+ translations: Translations;
21
+ }) {
22
+ this.locale = locale || FALLBACK_LOCALE;
23
+ this.translations = translations;
24
+ }
25
+
26
+ /**
27
+ * Get translation.
28
+ *
29
+ * @param str String to translate.
30
+ * @returns Translated string.
31
+ */
32
+ t(str: keyof Translations & string): string {
33
+ const translation = this.translations[str];
34
+ if (!translation) {
35
+ throw new Error(`${str} Translation string not found`);
36
+ }
37
+
38
+ const localized = translation[this.locale];
39
+ if (!localized) {
40
+ throw new Error(
41
+ `'${str}' translation not found for locale '${this.locale}'`,
42
+ );
43
+ }
44
+
45
+ return localized;
46
+ }
47
+ }
48
+
49
+ export { I18n };