@stats-forge/github-stats-forge-core 0.0.1 → 0.1.0
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/README.md +0 -10
- package/build/api/api-result.d.ts +6 -7
- package/build/api/api-result.d.ts.map +1 -1
- package/build/api/api-result.js +4 -5
- package/build/api/gist.d.ts +3 -10
- package/build/api/gist.d.ts.map +1 -1
- package/build/api/gist.js +4 -11
- package/build/api/index.d.ts +12 -12
- package/build/api/params.d.ts +11 -8
- package/build/api/params.d.ts.map +1 -1
- package/build/api/params.js +16 -14
- package/build/api/pin.d.ts +3 -19
- package/build/api/pin.d.ts.map +1 -1
- package/build/api/pin.js +6 -22
- package/build/api/stats.d.ts +10 -4
- package/build/api/stats.d.ts.map +1 -1
- package/build/api/stats.js +6 -32
- package/build/api/top-langs.d.ts +12 -6
- package/build/api/top-langs.d.ts.map +1 -1
- package/build/api/top-langs.js +6 -26
- package/build/api/wakatime.d.ts +13 -5
- package/build/api/wakatime.d.ts.map +1 -1
- package/build/api/wakatime.js +4 -21
- package/build/calculateRank.d.ts +0 -9
- package/build/calculateRank.d.ts.map +1 -1
- package/build/calculateRank.js +12 -17
- package/build/cards/gist.d.ts +2 -4
- package/build/cards/gist.d.ts.map +1 -1
- package/build/cards/gist.js +27 -36
- package/build/cards/options.d.ts +1 -1
- package/build/cards/repo.d.ts +2 -4
- package/build/cards/repo.d.ts.map +1 -1
- package/build/cards/repo.js +61 -93
- package/build/cards/stats.d.ts +3 -8
- package/build/cards/stats.d.ts.map +1 -1
- package/build/cards/stats.js +119 -165
- package/build/cards/top-languages.d.ts +5 -31
- package/build/cards/top-languages.d.ts.map +1 -1
- package/build/cards/top-languages.js +176 -384
- package/build/cards/wakatime.d.ts +4 -6
- package/build/cards/wakatime.d.ts.map +1 -1
- package/build/cards/wakatime.js +118 -185
- package/build/common/Card.d.ts +14 -62
- package/build/common/Card.d.ts.map +1 -1
- package/build/common/Card.js +97 -199
- package/build/common/I18n.d.ts +0 -6
- package/build/common/I18n.d.ts.map +1 -1
- package/build/common/I18n.js +3 -6
- package/build/common/color.d.ts +4 -17
- package/build/common/color.d.ts.map +1 -1
- package/build/common/color.js +67 -49
- package/build/common/config.d.ts +4 -14
- package/build/common/config.d.ts.map +1 -1
- package/build/common/config.js +3 -16
- package/build/common/constants.d.ts +4 -2
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/constants.js +3 -1
- package/build/common/date.d.ts +0 -2
- package/build/common/date.d.ts.map +1 -1
- package/build/common/date.js +0 -2
- package/build/common/emojiMap.d.ts +8 -0
- package/build/common/emojiMap.d.ts.map +1 -0
- package/build/common/emojiMap.js +10 -0
- package/build/common/emojiMap.json +1915 -0
- package/build/common/error.d.ts +1 -10
- package/build/common/error.d.ts.map +1 -1
- package/build/common/error.js +1 -10
- package/build/common/fmt.d.ts +3 -15
- package/build/common/fmt.d.ts.map +1 -1
- package/build/common/fmt.js +7 -35
- package/build/common/http.d.ts +2 -7
- package/build/common/http.d.ts.map +1 -1
- package/build/common/http.js +8 -17
- package/build/common/icons.d.ts +18 -20
- package/build/common/icons.d.ts.map +1 -1
- package/build/common/icons.js +103 -67
- package/build/common/languageColors.d.ts +0 -1
- package/build/common/languageColors.d.ts.map +1 -1
- package/build/common/languageColors.js +1 -4
- package/build/common/languageColors.json +18 -2
- package/build/common/ops.d.ts +3 -14
- package/build/common/ops.d.ts.map +1 -1
- package/build/common/ops.js +19 -30
- package/build/common/render.d.ts +28 -82
- package/build/common/render.d.ts.map +1 -1
- package/build/common/render.js +175 -211
- package/build/common/retryer.d.ts +2 -7
- package/build/common/retryer.d.ts.map +1 -1
- package/build/common/retryer.js +4 -11
- package/build/fetchers/gist.d.ts +2 -5
- package/build/fetchers/gist.d.ts.map +1 -1
- package/build/fetchers/gist.js +1 -5
- package/build/fetchers/index.d.ts +12 -12
- package/build/fetchers/repo.d.ts +2 -11
- package/build/fetchers/repo.d.ts.map +1 -1
- package/build/fetchers/repo.js +20 -33
- package/build/fetchers/stats.d.ts +2 -32
- package/build/fetchers/stats.d.ts.map +1 -1
- package/build/fetchers/stats.js +27 -86
- package/build/fetchers/top-languages.d.ts +2 -9
- package/build/fetchers/top-languages.d.ts.map +1 -1
- package/build/fetchers/top-languages.js +15 -31
- package/build/fetchers/types.d.ts +1 -1
- package/build/fetchers/wakatime.d.ts +2 -6
- package/build/fetchers/wakatime.d.ts.map +1 -1
- package/build/fetchers/wakatime.js +1 -5
- package/build/graphql/contributionsDocument.d.ts +3 -3
- package/build/graphql/contributionsDocument.d.ts.map +1 -1
- package/build/graphql/contributionsDocument.js +0 -1
- package/build/graphql/generated/gist.d.ts +1 -1
- package/build/graphql/generated/repo.d.ts +1 -1
- package/build/graphql/generated/stats.d.ts +3 -3
- package/build/graphql/generated/top-languages.d.ts +2 -2
- package/build/graphql/graphqlDocument.d.ts +0 -1
- package/build/graphql/graphqlDocument.d.ts.map +1 -1
- package/build/graphql/graphqlDocument.js +0 -1
- package/build/graphql/reposContributedToDocument.d.ts +4 -5
- package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
- package/build/graphql/reposContributedToDocument.js +0 -2
- package/build/markup/css.d.ts +35 -0
- package/build/markup/css.d.ts.map +1 -0
- package/build/markup/css.js +64 -0
- package/build/markup/element.d.ts +41 -0
- package/build/markup/element.d.ts.map +1 -0
- package/build/markup/element.js +127 -0
- package/build/markup/html.d.ts +13 -0
- package/build/markup/html.d.ts.map +1 -0
- package/build/markup/html.js +27 -0
- package/build/markup/index.d.ts +9 -0
- package/build/markup/index.d.ts.map +1 -0
- package/build/markup/index.js +6 -0
- package/build/markup/serialize.d.ts +16 -0
- package/build/markup/serialize.d.ts.map +1 -0
- package/build/markup/serialize.js +17 -0
- package/build/themes/index.d.ts +332 -333
- package/build/themes/index.d.ts.map +1 -1
- package/build/themes/index.js +1 -1
- package/build/translations.d.ts +2 -6
- package/build/translations.d.ts.map +1 -1
- package/build/translations.js +621 -628
- package/package.json +17 -26
- package/src/api/api-result.ts +10 -9
- package/src/api/gist.ts +11 -18
- package/src/api/index.ts +12 -12
- package/src/api/params.ts +27 -21
- package/src/api/pin.ts +13 -28
- package/src/api/stats.ts +13 -39
- package/src/api/top-langs.ts +13 -33
- package/src/api/wakatime.ts +11 -28
- package/src/calculateRank.ts +12 -23
- package/src/cards/gist.ts +40 -43
- package/src/cards/options.ts +1 -1
- package/src/cards/repo.ts +97 -107
- package/src/cards/stats.ts +165 -184
- package/src/cards/top-languages.ts +297 -413
- package/src/cards/wakatime.ts +193 -221
- package/src/common/Card.ts +165 -217
- package/src/common/I18n.ts +3 -6
- package/src/common/color.ts +80 -57
- package/src/common/config.ts +5 -18
- package/src/common/constants.ts +5 -2
- package/src/common/date.ts +0 -2
- package/src/common/emojiMap.json +1915 -0
- package/src/common/emojiMap.ts +13 -0
- package/src/common/error.ts +2 -11
- package/src/common/fmt.ts +7 -44
- package/src/common/http.ts +9 -15
- package/src/common/icons.ts +117 -42
- package/src/common/languageColors.json +18 -2
- package/src/common/languageColors.ts +1 -4
- package/src/common/ops.ts +26 -34
- package/src/common/render.ts +279 -232
- package/src/common/retryer.ts +8 -15
- package/src/fetchers/gist.ts +8 -12
- package/src/fetchers/index.ts +12 -12
- package/src/fetchers/repo.ts +27 -40
- package/src/fetchers/stats.ts +44 -102
- package/src/fetchers/top-languages.ts +26 -40
- package/src/fetchers/types.ts +1 -1
- package/src/fetchers/wakatime.ts +5 -9
- package/src/graphql/contributionsDocument.ts +7 -5
- package/src/graphql/generated/gist.ts +1 -1
- package/src/graphql/generated/repo.ts +1 -1
- package/src/graphql/generated/stats.ts +2 -2
- package/src/graphql/generated/top-languages.ts +2 -2
- package/src/graphql/graphqlDocument.ts +0 -1
- package/src/graphql/reposContributedToDocument.ts +6 -7
- package/src/markup/css.ts +118 -0
- package/src/markup/element.ts +246 -0
- package/src/markup/html.ts +34 -0
- package/src/markup/index.ts +9 -0
- package/src/markup/serialize.ts +27 -0
- package/src/themes/index.ts +1 -1
- package/src/translations.ts +622 -629
- package/build/cards/index.d.ts +0 -11
- package/build/cards/index.d.ts.map +0 -1
- package/build/cards/index.js +0 -7
- package/build/common/html.d.ts +0 -8
- package/build/common/html.d.ts.map +0 -1
- package/build/common/html.js +0 -14
- package/build/index.d.ts +0 -15
- package/build/index.d.ts.map +0 -1
- package/build/index.js +0 -10
- package/src/_emoji-name-map.d.ts +0 -10
- package/src/_github-username-regex.d.ts +0 -4
- package/src/cards/index.ts +0 -21
- package/src/common/html.ts +0 -17
- package/src/index.ts +0 -18
package/src/common/render.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import type { Child, Declarations, MarkupElement } from '../markup/index.ts';
|
|
2
|
+
import { el, renderMarkup, rule, style } from '../markup/index.ts';
|
|
3
|
+
|
|
4
|
+
import { getCardColors, isPrefixedHexColor } from './color.ts';
|
|
5
|
+
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from './error.ts';
|
|
6
|
+
import { kFormatter } from './fmt.ts';
|
|
7
|
+
import { clampValue } from './ops.ts';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Auto layout utility, allows us to layout things vertically or horizontally with
|
|
9
11
|
* proper gaping.
|
|
10
12
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
* @param props Function properties.
|
|
14
|
-
* @param props.items Array of sanitized items to layout.
|
|
15
|
-
* @param props.gap Gap between items.
|
|
16
|
-
* @param props.direction Direction to layout items.
|
|
17
|
-
* @param props.sizes Array of sizes for each item.
|
|
18
|
-
* @returns Array of items with proper layout.
|
|
13
|
+
* @returns The items, each in the group that positions it.
|
|
19
14
|
*/
|
|
20
15
|
const flexLayout = ({
|
|
21
16
|
items,
|
|
@@ -23,59 +18,60 @@ const flexLayout = ({
|
|
|
23
18
|
direction,
|
|
24
19
|
sizes = [],
|
|
25
20
|
}: {
|
|
26
|
-
items: Array<
|
|
21
|
+
items: Array<Child>;
|
|
27
22
|
gap: number;
|
|
28
23
|
direction?: 'column' | 'row';
|
|
29
24
|
sizes?: Array<number>;
|
|
30
|
-
}): Array<
|
|
25
|
+
}): Array<Child> => {
|
|
31
26
|
if (sizes.some((size) => !Number.isFinite(size))) {
|
|
32
27
|
throw new Error('flexLayout: `sizes` must contain only numbers');
|
|
33
28
|
}
|
|
34
29
|
|
|
30
|
+
const laidOut = items.filter(Boolean);
|
|
31
|
+
|
|
32
|
+
// A lone item is already where it belongs, so it needs no group to move it.
|
|
33
|
+
if (laidOut.length === 1) {
|
|
34
|
+
return laidOut;
|
|
35
|
+
}
|
|
36
|
+
|
|
35
37
|
let lastSize = 0;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
return laidOut.map((item, i) => {
|
|
39
|
+
const offset = lastSize;
|
|
40
|
+
lastSize += (sizes[i] ?? 0) + gap;
|
|
41
|
+
|
|
42
|
+
// The first item never moves, so it gets no transform to carry.
|
|
43
|
+
const transform =
|
|
44
|
+
offset === 0
|
|
45
|
+
? undefined
|
|
46
|
+
: direction === 'column'
|
|
47
|
+
? `translate(0, ${offset})`
|
|
48
|
+
: `translate(${offset}, 0)`;
|
|
49
|
+
|
|
50
|
+
return el('g', { transform }, item);
|
|
45
51
|
});
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
* Creates a node to display the primary programming language of the repository/gist.
|
|
50
56
|
*
|
|
51
|
-
* @param langName Language name.
|
|
52
|
-
* @param langColor Language color.
|
|
53
57
|
* @returns Language display SVG object.
|
|
54
58
|
*/
|
|
55
|
-
const createLanguageNode = (langName: string, langColor: string):
|
|
59
|
+
const createLanguageNode = (langName: string, langColor: string): MarkupElement => {
|
|
56
60
|
if (!isPrefixedHexColor(langColor)) {
|
|
57
61
|
throw new Error(`Invalid language color: "${langColor}"`);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
return el(
|
|
65
|
+
'g',
|
|
66
|
+
{ 'data-testid': 'primary-lang' },
|
|
67
|
+
el('circle', { 'data-testid': 'lang-color', cx: 0, cy: -5, r: 6, fill: langColor }),
|
|
68
|
+
el('text', { 'data-testid': 'lang-name', class: 'gray', x: 15 }, langName),
|
|
69
|
+
);
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
73
|
* Create a node to indicate progress in percentage along a horizontal line.
|
|
70
74
|
*
|
|
71
|
-
* @param params Object that contains the createProgressNode parameters.
|
|
72
|
-
* @param params.x X-axis position.
|
|
73
|
-
* @param params.y Y-axis position.
|
|
74
|
-
* @param params.width Width of progress bar.
|
|
75
|
-
* @param params.color Optional foreground color as an inline fill fallback.
|
|
76
|
-
* When omitted, the color must be set via a `.lang-progress` CSS rule.
|
|
77
|
-
* @param params.progress Progress value.
|
|
78
|
-
* @param params.delay Delay before animation starts.
|
|
79
75
|
* @returns Progress node.
|
|
80
76
|
*/
|
|
81
77
|
const createProgressNode = ({
|
|
@@ -89,42 +85,57 @@ const createProgressNode = ({
|
|
|
89
85
|
x: number;
|
|
90
86
|
y: number;
|
|
91
87
|
width: number;
|
|
88
|
+
/** When omitted, the color must be set via a `.lang-progress` CSS rule. */
|
|
92
89
|
color?: string;
|
|
93
90
|
progress: number;
|
|
94
91
|
delay: number;
|
|
95
|
-
}):
|
|
92
|
+
}): MarkupElement => {
|
|
96
93
|
if (color !== undefined && !isPrefixedHexColor(color)) {
|
|
97
94
|
throw new Error(`Invalid progress color: "${color}"`);
|
|
98
95
|
}
|
|
99
96
|
if (!Number.isFinite(width)) {
|
|
100
|
-
throw new
|
|
97
|
+
throw new TypeError(`Invalid width: "${width}"`);
|
|
101
98
|
}
|
|
102
99
|
if (!Number.isFinite(x)) {
|
|
103
|
-
throw new
|
|
100
|
+
throw new TypeError(`Invalid x: "${x}"`);
|
|
104
101
|
}
|
|
105
102
|
if (!Number.isFinite(y)) {
|
|
106
|
-
throw new
|
|
103
|
+
throw new TypeError(`Invalid y: "${y}"`);
|
|
107
104
|
}
|
|
108
105
|
if (!Number.isFinite(delay)) {
|
|
109
|
-
throw new
|
|
106
|
+
throw new TypeError(`Invalid delay: "${delay}"`);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
const progressPercentage = clampValue(progress, 2, 100);
|
|
113
110
|
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
return el(
|
|
112
|
+
'svg',
|
|
113
|
+
{ width, x, y },
|
|
114
|
+
el('rect', {
|
|
115
|
+
'data-testid': 'progress-background',
|
|
116
|
+
class: 'progress-background',
|
|
117
|
+
rx: 5,
|
|
118
|
+
ry: 5,
|
|
119
|
+
x: 0,
|
|
120
|
+
y: 0,
|
|
121
|
+
width,
|
|
122
|
+
height: 8,
|
|
123
|
+
}),
|
|
124
|
+
el(
|
|
125
|
+
'svg',
|
|
126
|
+
{ 'data-testid': 'lang-progress', width: `${progressPercentage}%` },
|
|
127
|
+
el('rect', {
|
|
128
|
+
height: 8,
|
|
129
|
+
fill: color,
|
|
130
|
+
rx: 5,
|
|
131
|
+
ry: 5,
|
|
132
|
+
x: 0,
|
|
133
|
+
y: 0,
|
|
134
|
+
class: 'lang-progress',
|
|
135
|
+
style: `animation-delay: ${delay}ms;`,
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
128
139
|
};
|
|
129
140
|
|
|
130
141
|
/**
|
|
@@ -132,15 +143,6 @@ const createProgressNode = ({
|
|
|
132
143
|
* native, font-aware wrapping. Content overflowing `lineCount` lines is
|
|
133
144
|
* clipped (with an ellipsis on the last visible line) by CSS line-clamp.
|
|
134
145
|
*
|
|
135
|
-
* @param props Function properties.
|
|
136
|
-
* @param props.text Text to render (will be HTML-encoded).
|
|
137
|
-
* @param props.x X position of the foreignObject.
|
|
138
|
-
* @param props.y Y position of the foreignObject.
|
|
139
|
-
* @param props.width Width of the wrap box.
|
|
140
|
-
* @param props.height Height of the wrap box.
|
|
141
|
-
* @param props.lineCount Maximum number of lines to display.
|
|
142
|
-
* @param props.className CSS class applied to the inner element.
|
|
143
|
-
* @param props.testId Optional test id for the inner element.
|
|
144
146
|
* @returns foreignObject SVG node.
|
|
145
147
|
*/
|
|
146
148
|
const wrappedTextNode = ({
|
|
@@ -161,31 +163,37 @@ const wrappedTextNode = ({
|
|
|
161
163
|
lineCount: number;
|
|
162
164
|
className: string;
|
|
163
165
|
testId?: string;
|
|
164
|
-
}):
|
|
166
|
+
}): MarkupElement => {
|
|
165
167
|
if (!Number.isFinite(x)) {
|
|
166
|
-
throw new
|
|
168
|
+
throw new TypeError(`Invalid x: "${x}"`);
|
|
167
169
|
}
|
|
168
170
|
if (!Number.isFinite(y)) {
|
|
169
|
-
throw new
|
|
171
|
+
throw new TypeError(`Invalid y: "${y}"`);
|
|
170
172
|
}
|
|
171
173
|
if (!Number.isFinite(width)) {
|
|
172
|
-
throw new
|
|
174
|
+
throw new TypeError(`Invalid width: "${width}"`);
|
|
173
175
|
}
|
|
174
176
|
if (!Number.isFinite(height)) {
|
|
175
|
-
throw new
|
|
177
|
+
throw new TypeError(`Invalid height: "${height}"`);
|
|
176
178
|
}
|
|
177
179
|
if (!Number.isFinite(lineCount)) {
|
|
178
|
-
throw new
|
|
180
|
+
throw new TypeError(`Invalid lineCount: "${lineCount}"`);
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
return el(
|
|
184
|
+
'foreignObject',
|
|
185
|
+
{ x, y, width, height },
|
|
186
|
+
el(
|
|
187
|
+
'div',
|
|
188
|
+
{
|
|
189
|
+
xmlns: 'http://www.w3.org/1999/xhtml',
|
|
190
|
+
class: className,
|
|
191
|
+
style: `--lines: ${lineCount};`,
|
|
192
|
+
'data-testid': testId,
|
|
193
|
+
},
|
|
194
|
+
text,
|
|
195
|
+
),
|
|
196
|
+
);
|
|
189
197
|
};
|
|
190
198
|
|
|
191
199
|
/**
|
|
@@ -194,69 +202,67 @@ const wrappedTextNode = ({
|
|
|
194
202
|
* browser handles wrapping and the line count is taken from the `--lines`
|
|
195
203
|
* custom property set on the element.
|
|
196
204
|
*
|
|
197
|
-
* @
|
|
198
|
-
* @returns CSS rules block (without the surrounding selector).
|
|
205
|
+
* @returns The declarations, to merge into the class's own rule.
|
|
199
206
|
*/
|
|
200
|
-
const wrappedTextStyles = (color: string):
|
|
207
|
+
const wrappedTextStyles = (color: string): Declarations => {
|
|
201
208
|
if (!isPrefixedHexColor(color)) {
|
|
202
209
|
throw new Error(`Invalid text color: "${color}"`);
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
212
|
+
return {
|
|
213
|
+
color,
|
|
214
|
+
margin: 0,
|
|
215
|
+
'line-height': 1.2,
|
|
216
|
+
'overflow-wrap': 'anywhere',
|
|
217
|
+
'word-break': 'break-word',
|
|
218
|
+
display: '-webkit-box',
|
|
219
|
+
'-webkit-box-orient': 'vertical',
|
|
220
|
+
'-webkit-line-clamp': 'var(--lines)',
|
|
221
|
+
'line-clamp': 'var(--lines)',
|
|
222
|
+
overflow: 'hidden',
|
|
223
|
+
'text-overflow': 'ellipsis',
|
|
224
|
+
'padding-bottom': '0.15em',
|
|
225
|
+
};
|
|
219
226
|
};
|
|
220
227
|
|
|
221
228
|
/**
|
|
222
229
|
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
|
|
230
|
+
* Draws nothing when the label is not positive.
|
|
223
231
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* @param icon The sanitized icon to display.
|
|
227
|
-
* @param label The label to display.
|
|
228
|
-
* @param testid The testid to assign to the label.
|
|
229
|
-
* @param iconSize The size of the icon.
|
|
230
|
-
* @returns Icon with label SVG object.
|
|
232
|
+
* @returns Icon with label SVG object, or nothing when the label is not positive.
|
|
231
233
|
*/
|
|
232
234
|
const iconWithLabel = (
|
|
233
|
-
icon:
|
|
235
|
+
icon: Child,
|
|
234
236
|
label: number | string,
|
|
235
237
|
testid: string,
|
|
236
238
|
iconSize: number,
|
|
237
|
-
):
|
|
239
|
+
): Child => {
|
|
238
240
|
if (typeof label === 'number' && label <= 0) {
|
|
239
|
-
return
|
|
241
|
+
return undefined;
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
if (!Number.isFinite(iconSize)) {
|
|
243
|
-
throw new
|
|
245
|
+
throw new TypeError(`Invalid iconSize: "${iconSize}"`);
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
248
|
+
return flexLayout({
|
|
249
|
+
items: [
|
|
250
|
+
el(
|
|
251
|
+
'svg',
|
|
252
|
+
{
|
|
253
|
+
class: 'icon',
|
|
254
|
+
y: -12,
|
|
255
|
+
viewBox: '0 0 16 16',
|
|
256
|
+
version: '1.1',
|
|
257
|
+
width: iconSize,
|
|
258
|
+
height: iconSize,
|
|
259
|
+
},
|
|
260
|
+
icon,
|
|
261
|
+
),
|
|
262
|
+
el('text', { 'data-testid': testid, class: 'gray' }, String(label)),
|
|
263
|
+
],
|
|
264
|
+
gap: 20,
|
|
265
|
+
});
|
|
260
266
|
};
|
|
261
267
|
|
|
262
268
|
/**
|
|
@@ -264,22 +270,8 @@ const iconWithLabel = (
|
|
|
264
270
|
* (a label and its value, optionally with an icon and link).
|
|
265
271
|
* Shared by the stats and repo cards.
|
|
266
272
|
*
|
|
267
|
-
* The caller must ensure that the passed `
|
|
273
|
+
* The caller must ensure that the passed `link` is properly sanitized!
|
|
268
274
|
*
|
|
269
|
-
* @param params Object that contains the createTextNode parameters.
|
|
270
|
-
* @param params.icon The sanitized icon to display.
|
|
271
|
-
* @param params.label The label to display.
|
|
272
|
-
* @param params.value The value to display.
|
|
273
|
-
* @param params.id The id of the stat.
|
|
274
|
-
* @param params.unitSymbol The unit symbol of the stat.
|
|
275
|
-
* @param params.index The index of the stat.
|
|
276
|
-
* @param params.showIcons Whether to show icons.
|
|
277
|
-
* @param params.shiftValuePos Number of pixels the value has to be shifted to the right.
|
|
278
|
-
* @param params.bold Whether to bold the label.
|
|
279
|
-
* @param params.numberFormat The format of numbers on card.
|
|
280
|
-
* @param params.numberPrecision The precision of numbers on card.
|
|
281
|
-
* @param params.link Sanitized url to link to.
|
|
282
|
-
* @param params.labelXOffset horizontal offset for label.
|
|
283
275
|
* @returns The stats card text item SVG object.
|
|
284
276
|
*/
|
|
285
277
|
const createTextNode = ({
|
|
@@ -291,13 +283,15 @@ const createTextNode = ({
|
|
|
291
283
|
index,
|
|
292
284
|
showIcons,
|
|
293
285
|
shiftValuePos,
|
|
286
|
+
valueAnchorX,
|
|
294
287
|
bold,
|
|
288
|
+
labelBold = bold,
|
|
295
289
|
numberFormat,
|
|
296
290
|
numberPrecision,
|
|
297
291
|
link,
|
|
298
292
|
labelXOffset = 25,
|
|
299
293
|
}: {
|
|
300
|
-
icon:
|
|
294
|
+
icon: Child;
|
|
301
295
|
label: string;
|
|
302
296
|
value: number | string;
|
|
303
297
|
id: string;
|
|
@@ -306,20 +300,25 @@ const createTextNode = ({
|
|
|
306
300
|
index: number;
|
|
307
301
|
showIcons: boolean;
|
|
308
302
|
shiftValuePos: number;
|
|
303
|
+
valueAnchorX?: number | undefined;
|
|
309
304
|
bold: boolean;
|
|
305
|
+
labelBold?: boolean;
|
|
310
306
|
numberFormat: string;
|
|
311
307
|
numberPrecision?: number | undefined;
|
|
312
308
|
link?: string | undefined;
|
|
313
309
|
labelXOffset?: number;
|
|
314
|
-
}):
|
|
310
|
+
}): MarkupElement => {
|
|
315
311
|
if (!Number.isFinite(labelXOffset)) {
|
|
316
|
-
throw new
|
|
312
|
+
throw new TypeError(`Invalid labelXOffset: "${labelXOffset}"`);
|
|
317
313
|
}
|
|
318
314
|
if (!Number.isFinite(shiftValuePos)) {
|
|
319
|
-
throw new
|
|
315
|
+
throw new TypeError(`Invalid shiftValuePos: "${shiftValuePos}"`);
|
|
320
316
|
}
|
|
321
317
|
if (!Number.isFinite(index)) {
|
|
322
|
-
throw new
|
|
318
|
+
throw new TypeError(`Invalid index: "${index}"`);
|
|
319
|
+
}
|
|
320
|
+
if (valueAnchorX !== undefined && !Number.isFinite(valueAnchorX)) {
|
|
321
|
+
throw new Error(`Invalid valueAnchorX: "${valueAnchorX}"`);
|
|
323
322
|
}
|
|
324
323
|
|
|
325
324
|
const precision =
|
|
@@ -331,54 +330,62 @@ const createTextNode = ({
|
|
|
331
330
|
const rawValue = numberFormat.toLowerCase() === 'long' || id === 'prs_merged_percentage';
|
|
332
331
|
const kValue = rawValue || typeof value !== 'number' ? value : kFormatter(value, precision);
|
|
333
332
|
|
|
334
|
-
const staggerDelay =
|
|
335
|
-
const boldClass = bold ? '
|
|
333
|
+
const staggerDelay = 120 + index * 40;
|
|
334
|
+
const boldClass = bold ? 'bold' : 'not_bold';
|
|
335
|
+
const labelBoldClass = labelBold ? 'bold' : 'not_bold';
|
|
336
336
|
const valueX = (showIcons ? 140 : 120) + (bold ? 5 : 0) + shiftValuePos;
|
|
337
337
|
const unit = unitSymbol ? ` ${unitSymbol}` : '';
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
338
|
+
|
|
339
|
+
const content = [
|
|
340
|
+
showIcons &&
|
|
341
|
+
el(
|
|
342
|
+
'svg',
|
|
343
|
+
{
|
|
344
|
+
'data-testid': 'icon',
|
|
345
|
+
class: 'icon',
|
|
346
|
+
viewBox: '0 0 16 16',
|
|
347
|
+
version: '1.1',
|
|
348
|
+
width: 16,
|
|
349
|
+
height: 16,
|
|
350
|
+
},
|
|
351
|
+
icon,
|
|
352
|
+
),
|
|
353
|
+
el(
|
|
354
|
+
'text',
|
|
355
|
+
{ class: `stat ${labelBoldClass}`, x: showIcons ? labelXOffset : undefined, y: 12.5 },
|
|
356
|
+
`${label}:`,
|
|
357
|
+
),
|
|
358
|
+
el(
|
|
359
|
+
'text',
|
|
360
|
+
{
|
|
361
|
+
class: `stat ${boldClass}`,
|
|
362
|
+
x: valueAnchorX ?? valueX,
|
|
363
|
+
'text-anchor': valueAnchorX === undefined ? undefined : 'end',
|
|
364
|
+
y: 12.5,
|
|
365
|
+
'data-testid': id,
|
|
366
|
+
},
|
|
367
|
+
`${String(kValue)}${unit}`,
|
|
368
|
+
),
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
return el(
|
|
372
|
+
'g',
|
|
373
|
+
{
|
|
374
|
+
class: 'stagger',
|
|
375
|
+
style: `animation-delay: ${staggerDelay}ms`,
|
|
376
|
+
transform: 'translate(25, 0)',
|
|
377
|
+
},
|
|
378
|
+
link ? el('a', { href: link }, content) : content,
|
|
379
|
+
);
|
|
362
380
|
};
|
|
363
381
|
|
|
364
|
-
// Script parameters.
|
|
365
382
|
const ERROR_CARD_LENGTH = 576.5;
|
|
366
383
|
|
|
367
|
-
const UPSTREAM_API_ERRORS = [TRY_AGAIN_LATER, SECONDARY_ERROR_MESSAGES.rate_limited];
|
|
384
|
+
const UPSTREAM_API_ERRORS = new Set([TRY_AGAIN_LATER, SECONDARY_ERROR_MESSAGES.rate_limited]);
|
|
368
385
|
|
|
369
386
|
/**
|
|
370
387
|
* Renders error message on the card.
|
|
371
388
|
*
|
|
372
|
-
* @param args Function arguments.
|
|
373
|
-
* @param args.message Main error message.
|
|
374
|
-
* @param args.secondaryMessage The secondary error message.
|
|
375
|
-
* @param args.renderOptions Render options.
|
|
376
|
-
* @param args.renderOptions.title_color Card title color.
|
|
377
|
-
* @param args.renderOptions.text_color Card text color.
|
|
378
|
-
* @param args.renderOptions.bg_color Card background color.
|
|
379
|
-
* @param args.renderOptions.border_color Card border color.
|
|
380
|
-
* @param args.renderOptions.theme Card theme.
|
|
381
|
-
* @param args.renderOptions.show_repo_link Whether to show repo link or not.
|
|
382
389
|
* @returns The SVG markup.
|
|
383
390
|
*/
|
|
384
391
|
const renderError = ({
|
|
@@ -417,37 +424,55 @@ const renderError = ({
|
|
|
417
424
|
theme,
|
|
418
425
|
});
|
|
419
426
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
: '
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
427
|
+
const hint =
|
|
428
|
+
UPSTREAM_API_ERRORS.has(secondaryMessage) || !show_repo_link
|
|
429
|
+
? ''
|
|
430
|
+
: ' file an issue at https://tinyurl.com/github-stats';
|
|
431
|
+
|
|
432
|
+
return renderMarkup(
|
|
433
|
+
el(
|
|
434
|
+
'svg',
|
|
435
|
+
{
|
|
436
|
+
width: ERROR_CARD_LENGTH,
|
|
437
|
+
height: 120,
|
|
438
|
+
viewBox: `0 0 ${ERROR_CARD_LENGTH} 120`,
|
|
439
|
+
fill: String(bgColor),
|
|
440
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
441
|
+
role: 'img',
|
|
442
|
+
'aria-labelledby': 'titleId descId',
|
|
443
|
+
},
|
|
444
|
+
el('title', { id: 'titleId' }, message),
|
|
445
|
+
el('desc', { id: 'descId' }, secondaryMessage),
|
|
446
|
+
style(
|
|
447
|
+
rule('.text', { font: "600 16px 'Segoe UI', Ubuntu, Sans-Serif", fill: titleColor }),
|
|
448
|
+
rule('.small', { font: "600 12px 'Segoe UI', Ubuntu, Sans-Serif", fill: textColor }),
|
|
449
|
+
rule('.gray', { fill: '#858585' }),
|
|
450
|
+
),
|
|
451
|
+
el('rect', {
|
|
452
|
+
x: 0.5,
|
|
453
|
+
y: 0.5,
|
|
454
|
+
width: ERROR_CARD_LENGTH - 1,
|
|
455
|
+
height: '99%',
|
|
456
|
+
rx: 8,
|
|
457
|
+
fill: String(bgColor),
|
|
458
|
+
stroke: borderColor,
|
|
459
|
+
}),
|
|
460
|
+
el('text', { x: 25, y: 45, class: 'text' }, `Something went wrong!${hint}`),
|
|
461
|
+
el(
|
|
462
|
+
'text',
|
|
463
|
+
{ 'data-testid': 'message', x: 25, y: 55, class: 'text small' },
|
|
464
|
+
el('tspan', { x: 25, dy: 18 }, message),
|
|
465
|
+
el('tspan', { x: 25, dy: 18, class: 'gray' }, secondaryMessage),
|
|
466
|
+
),
|
|
467
|
+
),
|
|
468
|
+
);
|
|
443
469
|
};
|
|
444
470
|
|
|
445
471
|
/**
|
|
446
472
|
* Retrieve text length based on Segoe UI font.
|
|
447
473
|
*
|
|
448
474
|
* @see https://stackoverflow.com/a/48172630/10629172
|
|
449
|
-
*
|
|
450
|
-
* @param fontSize Font size.
|
|
475
|
+
*
|
|
451
476
|
* @returns Text length.
|
|
452
477
|
*/
|
|
453
478
|
const measureText = (str: string, fontSize = 10): number => {
|
|
@@ -494,18 +519,17 @@ const measureText = (str: string, fontSize = 10): number => {
|
|
|
494
519
|
const cjkRange = /[\u3000-\u9FFF\uFF00-\uFFEF]/;
|
|
495
520
|
|
|
496
521
|
return (
|
|
497
|
-
str
|
|
498
|
-
.
|
|
499
|
-
|
|
500
|
-
if (cjkRange.test(c) || c === '\u3000') {
|
|
522
|
+
[...str]
|
|
523
|
+
.map((char) => {
|
|
524
|
+
if (cjkRange.test(char) || char === '\u3000') {
|
|
501
525
|
// CJK glyphs and U+3000 IDEOGRAPHIC SPACE are full-width by default;
|
|
502
526
|
return 1;
|
|
503
527
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return avg;
|
|
528
|
+
const code = char.codePointAt(0) ?? 0;
|
|
529
|
+
if (code < widths.length) {
|
|
530
|
+
return widths[code] ?? avg;
|
|
508
531
|
}
|
|
532
|
+
return avg;
|
|
509
533
|
})
|
|
510
534
|
.reduce((cur, acc) => acc + cur) * fontSize
|
|
511
535
|
);
|
|
@@ -518,9 +542,6 @@ const measureText = (str: string, fontSize = 10): number => {
|
|
|
518
542
|
* The browser still does the real wrap inside the foreignObject; this is only
|
|
519
543
|
* used to size the SVG.
|
|
520
544
|
*
|
|
521
|
-
* @param text Text to split.
|
|
522
|
-
* @param fontSize Font size in px (matches `measureText`).
|
|
523
|
-
* @param maxWidth Available wrap width in px.
|
|
524
545
|
* @returns Estimated wrapped lines.
|
|
525
546
|
*/
|
|
526
547
|
const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Array<string> => {
|
|
@@ -538,7 +559,7 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
538
559
|
// Korean Hangul (U+AC00–U+D7AF) is intentionally NOT in the CJK range
|
|
539
560
|
// because Korean wraps at word boundaries by default in HTML.
|
|
540
561
|
// ASCII whitespace is collapsed to a single space per CSS `white-space: normal;`
|
|
541
|
-
const normalizedText = text.
|
|
562
|
+
const normalizedText = text.replaceAll(/[\t\n\r ]+/g, ' ');
|
|
542
563
|
const tokens = normalizedText.match(
|
|
543
564
|
/\s|[\u3000-\u9FFF\uFF00-\uFFEF]|[^\s\u3000-\u9FFF\uFF00-\uFFEF]+/g,
|
|
544
565
|
);
|
|
@@ -546,8 +567,11 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
546
567
|
return [];
|
|
547
568
|
}
|
|
548
569
|
|
|
549
|
-
const takeFittingSegment = (
|
|
550
|
-
|
|
570
|
+
const takeFittingSegment = (
|
|
571
|
+
token: string,
|
|
572
|
+
availableWidth: number,
|
|
573
|
+
): { segment: string; width: number } => {
|
|
574
|
+
const characters = [...token];
|
|
551
575
|
let segment = '';
|
|
552
576
|
let width = 0;
|
|
553
577
|
|
|
@@ -575,7 +599,7 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
575
599
|
if (currentWidth === 0) {
|
|
576
600
|
continue;
|
|
577
601
|
}
|
|
578
|
-
lines[lines.length - 1] = (lines
|
|
602
|
+
lines[lines.length - 1] = (lines.at(-1) ?? '') + token;
|
|
579
603
|
currentWidth += measureText(token, fontSize);
|
|
580
604
|
continue;
|
|
581
605
|
}
|
|
@@ -585,7 +609,7 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
585
609
|
while (remaining) {
|
|
586
610
|
const w = measureText(remaining, fontSize);
|
|
587
611
|
if (currentWidth + w <= maxWidth) {
|
|
588
|
-
lines[lines.length - 1] = (lines
|
|
612
|
+
lines[lines.length - 1] = (lines.at(-1) ?? '') + remaining;
|
|
589
613
|
currentWidth += w;
|
|
590
614
|
break;
|
|
591
615
|
}
|
|
@@ -598,7 +622,7 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
598
622
|
|
|
599
623
|
// An atom wider than the box wraps mid-glyph (overflow-wrap: anywhere).
|
|
600
624
|
const { segment, width } = takeFittingSegment(remaining, maxWidth);
|
|
601
|
-
lines[lines.length - 1] = (lines
|
|
625
|
+
lines[lines.length - 1] = (lines.at(-1) ?? '') + segment;
|
|
602
626
|
currentWidth = width;
|
|
603
627
|
remaining = remaining.slice(segment.length);
|
|
604
628
|
}
|
|
@@ -607,14 +631,38 @@ const splitWrappedText = (text: string, fontSize: number, maxWidth: number): Arr
|
|
|
607
631
|
return lines.map((line) => line.replace(/[\t\n\r ]+$/, ''));
|
|
608
632
|
};
|
|
609
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Split text over multiple lines based on the card width.
|
|
636
|
+
*
|
|
637
|
+
* @returns Array of lines.
|
|
638
|
+
*/
|
|
639
|
+
const wrapTextMultiline = (
|
|
640
|
+
text: string,
|
|
641
|
+
width: number,
|
|
642
|
+
fontSize: number,
|
|
643
|
+
maxLines = 3,
|
|
644
|
+
): Array<string> => {
|
|
645
|
+
const wrapped = splitWrappedText(text, fontSize, width);
|
|
646
|
+
const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
|
|
647
|
+
|
|
648
|
+
// Add "..." to the last line if the text exceeds maxLines
|
|
649
|
+
if (wrapped.length > maxLines) {
|
|
650
|
+
const lastIndex = maxLines - 1;
|
|
651
|
+
const lastLine = lines[lastIndex];
|
|
652
|
+
if (lastLine !== undefined) {
|
|
653
|
+
lines[lastIndex] = `${lastLine}...`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Remove empty lines if text fits in less than maxLines lines
|
|
658
|
+
const multiLineText = lines.filter(Boolean);
|
|
659
|
+
return multiLineText;
|
|
660
|
+
};
|
|
661
|
+
|
|
610
662
|
/**
|
|
611
663
|
* Estimate how many lines a string will wrap to when laid out greedily at the
|
|
612
664
|
* given font size inside a box of width `maxWidth`, capped at `maxLines`.
|
|
613
665
|
*
|
|
614
|
-
* @param text Text to estimate.
|
|
615
|
-
* @param fontSize Font size in px (matches `measureText`).
|
|
616
|
-
* @param maxWidth Available wrap width in px.
|
|
617
|
-
* @param maxLines Cap on the returned line count.
|
|
618
666
|
* @returns Estimated line count, at least 1, at most `maxLines`.
|
|
619
667
|
*/
|
|
620
668
|
const countWrappedLines = (
|
|
@@ -622,9 +670,7 @@ const countWrappedLines = (
|
|
|
622
670
|
fontSize: number,
|
|
623
671
|
maxWidth: number,
|
|
624
672
|
maxLines: number,
|
|
625
|
-
): number =>
|
|
626
|
-
return Math.min(Math.max(1, splitWrappedText(text, fontSize, maxWidth).length), maxLines);
|
|
627
|
-
};
|
|
673
|
+
): number => Math.min(Math.max(1, splitWrappedText(text, fontSize, maxWidth).length), maxLines);
|
|
628
674
|
|
|
629
675
|
export {
|
|
630
676
|
renderError,
|
|
@@ -635,6 +681,7 @@ export {
|
|
|
635
681
|
flexLayout,
|
|
636
682
|
measureText,
|
|
637
683
|
splitWrappedText,
|
|
684
|
+
wrapTextMultiline,
|
|
638
685
|
countWrappedLines,
|
|
639
686
|
wrappedTextNode,
|
|
640
687
|
wrappedTextStyles,
|