@stats-forge/github-stats-forge-core 0.0.2 → 0.2.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.
Files changed (249) hide show
  1. package/README.md +0 -10
  2. package/build/api/api-result.d.ts +6 -7
  3. package/build/api/api-result.d.ts.map +1 -1
  4. package/build/api/api-result.js +4 -5
  5. package/build/api/contributed-to.d.ts +23 -0
  6. package/build/api/contributed-to.d.ts.map +1 -0
  7. package/build/api/contributed-to.js +43 -0
  8. package/build/api/gist.d.ts +8 -22
  9. package/build/api/gist.d.ts.map +1 -1
  10. package/build/api/gist.js +14 -36
  11. package/build/api/handler.d.ts +16 -0
  12. package/build/api/handler.d.ts.map +1 -0
  13. package/build/api/handler.js +29 -0
  14. package/build/api/index.d.ts +13 -12
  15. package/build/api/index.d.ts.map +1 -1
  16. package/build/api/index.js +1 -0
  17. package/build/api/params.d.ts +21 -12
  18. package/build/api/params.d.ts.map +1 -1
  19. package/build/api/params.js +41 -19
  20. package/build/api/pin.d.ts +11 -31
  21. package/build/api/pin.d.ts.map +1 -1
  22. package/build/api/pin.js +35 -63
  23. package/build/api/stats.d.ts +15 -16
  24. package/build/api/stats.d.ts.map +1 -1
  25. package/build/api/stats.js +60 -99
  26. package/build/api/top-langs.d.ts +13 -18
  27. package/build/api/top-langs.d.ts.map +1 -1
  28. package/build/api/top-langs.js +35 -72
  29. package/build/api/wakatime.d.ts +11 -17
  30. package/build/api/wakatime.d.ts.map +1 -1
  31. package/build/api/wakatime.js +27 -64
  32. package/build/calculateRank.d.ts +0 -9
  33. package/build/calculateRank.d.ts.map +1 -1
  34. package/build/calculateRank.js +12 -17
  35. package/build/cards/contributed-to.d.ts +19 -0
  36. package/build/cards/contributed-to.d.ts.map +1 -0
  37. package/build/cards/contributed-to.js +242 -0
  38. package/build/cards/gist.d.ts +3 -4
  39. package/build/cards/gist.d.ts.map +1 -1
  40. package/build/cards/gist.js +38 -43
  41. package/build/cards/options.d.ts +1 -1
  42. package/build/cards/repo.d.ts +14 -5
  43. package/build/cards/repo.d.ts.map +1 -1
  44. package/build/cards/repo.js +93 -103
  45. package/build/cards/stats.d.ts +18 -11
  46. package/build/cards/stats.d.ts.map +1 -1
  47. package/build/cards/stats.js +191 -222
  48. package/build/cards/top-languages.d.ts +18 -33
  49. package/build/cards/top-languages.d.ts.map +1 -1
  50. package/build/cards/top-languages.js +196 -387
  51. package/build/cards/wakatime.d.ts +17 -8
  52. package/build/cards/wakatime.d.ts.map +1 -1
  53. package/build/cards/wakatime.js +133 -187
  54. package/build/common/Card.d.ts +20 -59
  55. package/build/common/Card.d.ts.map +1 -1
  56. package/build/common/Card.js +131 -198
  57. package/build/common/I18n.d.ts +1 -7
  58. package/build/common/I18n.d.ts.map +1 -1
  59. package/build/common/I18n.js +7 -8
  60. package/build/common/brand.d.ts +76 -0
  61. package/build/common/brand.d.ts.map +1 -0
  62. package/build/common/brand.js +73 -0
  63. package/build/common/color.d.ts +4 -17
  64. package/build/common/color.d.ts.map +1 -1
  65. package/build/common/color.js +67 -49
  66. package/build/common/config.d.ts +4 -14
  67. package/build/common/config.d.ts.map +1 -1
  68. package/build/common/config.js +3 -16
  69. package/build/common/constants.d.ts +6 -2
  70. package/build/common/constants.d.ts.map +1 -1
  71. package/build/common/constants.js +5 -1
  72. package/build/common/date.d.ts +50 -5
  73. package/build/common/date.d.ts.map +1 -1
  74. package/build/common/date.js +104 -4
  75. package/build/common/emojiMap.d.ts +8 -0
  76. package/build/common/emojiMap.d.ts.map +1 -0
  77. package/build/common/emojiMap.js +10 -0
  78. package/build/common/emojiMap.json +1915 -0
  79. package/build/common/error.d.ts +1 -10
  80. package/build/common/error.d.ts.map +1 -1
  81. package/build/common/error.js +1 -10
  82. package/build/common/fmt.d.ts +3 -15
  83. package/build/common/fmt.d.ts.map +1 -1
  84. package/build/common/fmt.js +7 -35
  85. package/build/common/http.d.ts +2 -7
  86. package/build/common/http.d.ts.map +1 -1
  87. package/build/common/http.js +8 -17
  88. package/build/common/icons.d.ts +20 -20
  89. package/build/common/icons.d.ts.map +1 -1
  90. package/build/common/icons.js +108 -67
  91. package/build/common/languageColors.d.ts +0 -1
  92. package/build/common/languageColors.d.ts.map +1 -1
  93. package/build/common/languageColors.js +1 -4
  94. package/build/common/languageColors.json +18 -2
  95. package/build/common/ops.d.ts +11 -17
  96. package/build/common/ops.d.ts.map +1 -1
  97. package/build/common/ops.js +22 -39
  98. package/build/common/render.d.ts +34 -82
  99. package/build/common/render.d.ts.map +1 -1
  100. package/build/common/render.js +225 -259
  101. package/build/common/retryer.d.ts +2 -7
  102. package/build/common/retryer.d.ts.map +1 -1
  103. package/build/common/retryer.js +4 -11
  104. package/build/fetchers/contributed-to.d.ts +62 -0
  105. package/build/fetchers/contributed-to.d.ts.map +1 -0
  106. package/build/fetchers/contributed-to.js +184 -0
  107. package/build/fetchers/gist.d.ts +2 -5
  108. package/build/fetchers/gist.d.ts.map +1 -1
  109. package/build/fetchers/gist.js +1 -5
  110. package/build/fetchers/graphql-error.d.ts +13 -0
  111. package/build/fetchers/graphql-error.d.ts.map +1 -0
  112. package/build/fetchers/graphql-error.js +29 -0
  113. package/build/fetchers/index.d.ts +13 -12
  114. package/build/fetchers/index.d.ts.map +1 -1
  115. package/build/fetchers/index.js +1 -0
  116. package/build/fetchers/repo.d.ts +2 -11
  117. package/build/fetchers/repo.d.ts.map +1 -1
  118. package/build/fetchers/repo.js +20 -33
  119. package/build/fetchers/stats.d.ts +16 -40
  120. package/build/fetchers/stats.d.ts.map +1 -1
  121. package/build/fetchers/stats.js +89 -271
  122. package/build/fetchers/top-languages.d.ts +2 -9
  123. package/build/fetchers/top-languages.d.ts.map +1 -1
  124. package/build/fetchers/top-languages.js +16 -47
  125. package/build/fetchers/types.d.ts +25 -1
  126. package/build/fetchers/types.d.ts.map +1 -1
  127. package/build/fetchers/wakatime.d.ts +2 -6
  128. package/build/fetchers/wakatime.d.ts.map +1 -1
  129. package/build/fetchers/wakatime.js +1 -5
  130. package/build/graphql/contributionsCollection.d.ts +9 -0
  131. package/build/graphql/contributionsCollection.d.ts.map +1 -0
  132. package/build/graphql/contributionsCollection.js +12 -0
  133. package/build/graphql/contributionsDocument.d.ts +6 -6
  134. package/build/graphql/contributionsDocument.d.ts.map +1 -1
  135. package/build/graphql/contributionsDocument.js +8 -16
  136. package/build/graphql/generated/contributed-to.d.ts +22 -0
  137. package/build/graphql/generated/contributed-to.d.ts.map +1 -0
  138. package/build/graphql/generated/contributed-to.js +11 -0
  139. package/build/graphql/generated/gist.d.ts +1 -1
  140. package/build/graphql/generated/repo.d.ts +1 -1
  141. package/build/graphql/generated/stats.d.ts +16 -4
  142. package/build/graphql/generated/stats.d.ts.map +1 -1
  143. package/build/graphql/generated/stats.js +2 -2
  144. package/build/graphql/generated/top-languages.d.ts +2 -2
  145. package/build/graphql/graphqlDocument.d.ts +1 -2
  146. package/build/graphql/graphqlDocument.d.ts.map +1 -1
  147. package/build/graphql/graphqlDocument.js +0 -1
  148. package/build/graphql/reposContributedToDocument.d.ts +4 -5
  149. package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
  150. package/build/graphql/reposContributedToDocument.js +11 -7
  151. package/build/markup/css.d.ts +35 -0
  152. package/build/markup/css.d.ts.map +1 -0
  153. package/build/markup/css.js +64 -0
  154. package/build/markup/element.d.ts +41 -0
  155. package/build/markup/element.d.ts.map +1 -0
  156. package/build/markup/element.js +127 -0
  157. package/build/markup/html.d.ts +13 -0
  158. package/build/markup/html.d.ts.map +1 -0
  159. package/build/markup/html.js +27 -0
  160. package/build/markup/index.d.ts +9 -0
  161. package/build/markup/index.d.ts.map +1 -0
  162. package/build/markup/index.js +6 -0
  163. package/build/markup/serialize.d.ts +16 -0
  164. package/build/markup/serialize.d.ts.map +1 -0
  165. package/build/markup/serialize.js +17 -0
  166. package/build/themes/index.d.ts +332 -333
  167. package/build/themes/index.d.ts.map +1 -1
  168. package/build/themes/index.js +1 -1
  169. package/build/translations.d.ts +66 -7
  170. package/build/translations.d.ts.map +1 -1
  171. package/build/translations.js +686 -629
  172. package/package.json +17 -26
  173. package/src/api/api-result.ts +10 -9
  174. package/src/api/contributed-to.ts +84 -0
  175. package/src/api/gist.ts +22 -46
  176. package/src/api/handler.ts +42 -0
  177. package/src/api/index.ts +13 -12
  178. package/src/api/params.ts +74 -29
  179. package/src/api/pin.ts +38 -65
  180. package/src/api/stats.ts +81 -118
  181. package/src/api/top-langs.ts +38 -80
  182. package/src/api/wakatime.ts +35 -73
  183. package/src/calculateRank.ts +12 -23
  184. package/src/cards/contributed-to.ts +373 -0
  185. package/src/cards/gist.ts +55 -49
  186. package/src/cards/options.ts +1 -1
  187. package/src/cards/repo.ts +134 -120
  188. package/src/cards/stats.ts +262 -252
  189. package/src/cards/top-languages.ts +315 -422
  190. package/src/cards/wakatime.ts +207 -225
  191. package/src/common/Card.ts +207 -214
  192. package/src/common/I18n.ts +7 -8
  193. package/src/common/brand.ts +109 -0
  194. package/src/common/color.ts +80 -57
  195. package/src/common/config.ts +5 -18
  196. package/src/common/constants.ts +8 -2
  197. package/src/common/date.ts +135 -5
  198. package/src/common/emojiMap.json +1915 -0
  199. package/src/common/emojiMap.ts +13 -0
  200. package/src/common/error.ts +2 -11
  201. package/src/common/fmt.ts +7 -44
  202. package/src/common/http.ts +9 -15
  203. package/src/common/icons.ts +122 -42
  204. package/src/common/languageColors.json +18 -2
  205. package/src/common/languageColors.ts +1 -4
  206. package/src/common/ops.ts +31 -48
  207. package/src/common/render.ts +334 -284
  208. package/src/common/retryer.ts +8 -15
  209. package/src/fetchers/contributed-to.ts +271 -0
  210. package/src/fetchers/gist.ts +8 -12
  211. package/src/fetchers/graphql-error.ts +36 -0
  212. package/src/fetchers/index.ts +15 -12
  213. package/src/fetchers/repo.ts +27 -40
  214. package/src/fetchers/stats.ts +157 -366
  215. package/src/fetchers/top-languages.ts +27 -55
  216. package/src/fetchers/types.ts +27 -1
  217. package/src/fetchers/wakatime.ts +5 -9
  218. package/src/graphql/contributionsCollection.ts +18 -0
  219. package/src/graphql/contributionsDocument.ts +14 -19
  220. package/src/graphql/generated/contributed-to.ts +25 -0
  221. package/src/graphql/generated/gist.ts +1 -1
  222. package/src/graphql/generated/repo.ts +1 -1
  223. package/src/graphql/generated/stats.ts +21 -8
  224. package/src/graphql/generated/top-languages.ts +2 -2
  225. package/src/graphql/graphqlDocument.ts +1 -2
  226. package/src/graphql/queries/contributed-to.graphql +8 -0
  227. package/src/graphql/queries/stats.graphql +14 -2
  228. package/src/graphql/reposContributedToDocument.ts +16 -15
  229. package/src/markup/css.ts +118 -0
  230. package/src/markup/element.ts +246 -0
  231. package/src/markup/html.ts +34 -0
  232. package/src/markup/index.ts +9 -0
  233. package/src/markup/serialize.ts +27 -0
  234. package/src/themes/index.ts +1 -1
  235. package/src/translations.ts +699 -629
  236. package/build/cards/index.d.ts +0 -11
  237. package/build/cards/index.d.ts.map +0 -1
  238. package/build/cards/index.js +0 -7
  239. package/build/common/html.d.ts +0 -8
  240. package/build/common/html.d.ts.map +0 -1
  241. package/build/common/html.js +0 -14
  242. package/build/index.d.ts +0 -15
  243. package/build/index.d.ts.map +0 -1
  244. package/build/index.js +0 -10
  245. package/src/_emoji-name-map.d.ts +0 -10
  246. package/src/_github-username-regex.d.ts +0 -4
  247. package/src/cards/index.ts +0 -21
  248. package/src/common/html.ts +0 -17
  249. package/src/index.ts +0 -18
@@ -1,38 +1,33 @@
1
- import { isThemeName, themes } from '../themes/index.js';
1
+ import { isThemeName, themes } from '../themes/index.ts';
2
2
 
3
3
  /** Matches a 3-, 4-, 6-, or 8-digit hex color with no leading `#`. */
4
- const HEX_COLOR = /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$/;
4
+ const HEX_COLOR = /^(?<digits>[A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$/;
5
5
 
6
6
  /**
7
7
  * Checks if a value is a bare hex color, i.e. hex digits with no `#` prefix
8
8
  * (`"f00"`, `"ffffff"`). This is the form user-supplied color params and
9
9
  * gradient stops arrive in.
10
10
  *
11
- * @param value Value to check.
12
11
  * @returns True if the value is a bare hex color.
13
12
  */
14
- const isBareHexColor = (value: unknown): boolean => {
15
- return typeof value === 'string' && HEX_COLOR.test(value);
16
- };
13
+ const isBareHexColor = (value: unknown): boolean =>
14
+ typeof value === 'string' && HEX_COLOR.test(value);
17
15
 
18
16
  /**
19
17
  * Checks if a value is a `#`-prefixed hex color (`"#f00"`, `"#ffffff"`). This
20
18
  * is the form colors take once resolved by {@link getCardColors}, i.e. right
21
19
  * before they are written into the SVG.
22
20
  *
23
- * @param value Value to check.
24
21
  * @returns True if the value is a `#`-prefixed hex color.
25
22
  */
26
- const isPrefixedHexColor = (value: unknown): boolean => {
27
- return typeof value === 'string' && value.startsWith('#') && HEX_COLOR.test(value.slice(1));
28
- };
23
+ const isPrefixedHexColor = (value: unknown): boolean =>
24
+ typeof value === 'string' && value.startsWith('#') && HEX_COLOR.test(value.slice(1));
29
25
 
30
26
  /**
31
27
  * Checks if the given parts form a valid gradient: a finite numeric angle
32
28
  * followed by at least two bare-hex color stops, e.g. `["90", "f00", "0f0"]`.
33
29
  * The angle is written into the SVG `gradientTransform="rotate(...)"`.
34
30
  *
35
- * @param parts Gradient parts: `[angle, ...stops]`.
36
31
  * @returns True if the parts form a valid gradient.
37
32
  */
38
33
  const isValidGradient = (parts: Array<string>): boolean => {
@@ -42,14 +37,13 @@ const isValidGradient = (parts: Array<string>): boolean => {
42
37
  angle !== undefined &&
43
38
  angle.trim() !== '' &&
44
39
  Number.isFinite(Number(angle)) &&
45
- stops.every(isBareHexColor)
40
+ stops.every((stop) => isBareHexColor(stop))
46
41
  );
47
42
  };
48
43
 
49
44
  /**
50
45
  * Checks if a string is a valid input for a color or gradient.
51
46
  *
52
- * @param color String to check, may be null or undefined.
53
47
  * @returns True if the given string is a valid input.
54
48
  */
55
49
  const isValidColorInput = (color: string | null | undefined): boolean => {
@@ -62,13 +56,11 @@ const isValidColorInput = (color: string | null | undefined): boolean => {
62
56
  /**
63
57
  * Retrieves a gradient if color has more than one valid hex codes else a single color.
64
58
  *
65
- * @param color The color to parse.
66
- * @param fallbackColor The fallback color.
67
59
  * @returns The gradient or color.
68
60
  */
69
61
  const fallbackColor = (
70
62
  color: string | undefined,
71
- fallbackColor: string | Array<string>,
63
+ fallback: string | Array<string>,
72
64
  ): string | Array<string> => {
73
65
  const colors = color ? color.split(',') : [];
74
66
  if (colors.length > 1 && isValidGradient(colors)) {
@@ -79,7 +71,46 @@ const fallbackColor = (
79
71
  return `#${color}`;
80
72
  }
81
73
 
82
- return fallbackColor;
74
+ return fallback;
75
+ };
76
+
77
+ /** Border a light background gets when neither the user nor the theme names one. */
78
+ const LIGHT_BG_BORDER = '#0000001f';
79
+ /** Border a dark background gets when neither the user nor the theme names one. */
80
+ const DARK_BG_BORDER = '#ffffff26';
81
+ /** Border a see-through background gets: neutral, since the page behind it decides the contrast. */
82
+ const TRANSLUCENT_BG_BORDER = '#8b949e59';
83
+
84
+ /**
85
+ * @returns Its `[r, g, b, a]` channels, each on 0–1.
86
+ */
87
+ const hexChannels = (hex: string): [number, number, number, number] => {
88
+ const digits = hex.slice(1);
89
+ const expanded = digits.length <= 4 ? [...digits].map((digit) => digit + digit).join('') : digits;
90
+ const channel = (index: number): number =>
91
+ Number.parseInt(expanded.slice(index * 2, index * 2 + 2), 16) / 255;
92
+
93
+ return [channel(0), channel(1), channel(2), expanded.length === 8 ? channel(3) : 1];
94
+ };
95
+
96
+ /**
97
+ * Derives a border from the background, so a dark card never gets a light hairline.
98
+ *
99
+ * @returns A translucent `#`-prefixed hex border color.
100
+ */
101
+ const borderColorFor = (bgColor: string | Array<string>): string => {
102
+ // A gradient is judged by its first stop.
103
+ const hex = typeof bgColor === 'string' ? bgColor : `#${bgColor[1] ?? ''}`;
104
+ if (!isPrefixedHexColor(hex)) {
105
+ return LIGHT_BG_BORDER;
106
+ }
107
+
108
+ const [r, g, b, a] = hexChannels(hex);
109
+ if (a < 0.5) {
110
+ return TRANSLUCENT_BG_BORDER;
111
+ }
112
+
113
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5 ? DARK_BG_BORDER : LIGHT_BG_BORDER;
83
114
  };
84
115
 
85
116
  /**
@@ -134,15 +165,6 @@ type ColorInput = Partial<Record<BaseColorKey, string | undefined>>;
134
165
  /**
135
166
  * Returns theme based colors with proper overrides and defaults.
136
167
  *
137
- * @param props Function arguments.
138
- * @param props.title_color Card title color.
139
- * @param props.text_color Card text color.
140
- * @param props.icon_color Card icon color.
141
- * @param props.bg_color Card background color.
142
- * @param props.border_color Card border color.
143
- * @param props.ring_color Card ring color.
144
- * @param props.prog_bar_bg_color Progress bar background color.
145
- * @param props.theme Card theme.
146
168
  * @returns Card colors.
147
169
  */
148
170
  const getCardColors = ({
@@ -158,29 +180,24 @@ const getCardColors = ({
158
180
  const defaultTheme = themes.default;
159
181
  const selectedTheme = isThemeName(theme) ? themes[theme] : defaultTheme;
160
182
 
161
- const defaultBorderColor =
162
- 'border_color' in selectedTheme ? selectedTheme.border_color : defaultTheme.border_color;
183
+ const themeBorderColor = 'border_color' in selectedTheme ? selectedTheme.border_color : undefined;
163
184
 
164
- // get the color provided by the user else the theme color
165
- // finally if both colors are invalid fallback to default theme
185
+ // Each color is the user's, else the selected theme's, else the default theme's.
166
186
  const titleColor = fallbackColor(
167
187
  title_color || selectedTheme.title_color,
168
- '#' + defaultTheme.title_color,
188
+ `#${defaultTheme.title_color}`,
169
189
  );
170
-
171
- // get the color provided by the user else the theme color
172
- // finally if both colors are invalid we use the titleColor
173
190
  const iconColor = fallbackColor(
174
191
  icon_color || selectedTheme.icon_color,
175
- '#' + defaultTheme.icon_color,
192
+ `#${defaultTheme.icon_color}`,
176
193
  );
177
194
  const textColor = fallbackColor(
178
195
  text_color || selectedTheme.text_color,
179
- '#' + defaultTheme.text_color,
196
+ `#${defaultTheme.text_color}`,
180
197
  );
181
- const bgColor = fallbackColor(bg_color || selectedTheme.bg_color, '#' + defaultTheme.bg_color);
198
+ const bgColor = fallbackColor(bg_color || selectedTheme.bg_color, `#${defaultTheme.bg_color}`);
182
199
 
183
- const borderColor = fallbackColor(border_color || defaultBorderColor, '#' + defaultBorderColor);
200
+ const borderColor = fallbackColor(border_color || themeBorderColor, borderColorFor(bgColor));
184
201
  // No theme defines `ring_color`, so it falls back to the title color.
185
202
  const ringColor = fallbackColor(ring_color, titleColor);
186
203
  // No theme defines `prog_bar_bg_color`, so it falls back to "#ddd".
@@ -194,7 +211,7 @@ const getCardColors = ({
194
211
  typeof iconColor !== 'string' ||
195
212
  typeof borderColor !== 'string'
196
213
  ) {
197
- throw new Error('Unexpected behavior, all colors except background should be string.');
214
+ throw new TypeError('Unexpected behavior, all colors except background should be string.');
198
215
  }
199
216
 
200
217
  return {
@@ -214,14 +231,29 @@ type LightDarkColorParams = Partial<Record<`${BaseColorKey}_${ThemeVariant}`, st
214
231
  * Returns the light- or dark-mode-specific color params, given a set of
215
232
  * raw query params. Also removes the "_light" or "_dark" suffixes.
216
233
  *
217
- * @param params Raw query params with optional `_light` / `_dark` suffixes.
218
- * @param suffix `"_light"` or `"_dark"`.
219
234
  * @returns ColorInput with the suffix stripped, ready for `getCardColors`.
220
235
  */
221
236
  const extractLightDarkColors = (
222
237
  params: LightDarkColorParams,
223
238
  suffix: `_${ThemeVariant}`,
224
- ): ColorInput => Object.fromEntries(BASE_COLOR_KEYS.map((key) => [key, params[`${key}${suffix}`]]));
239
+ ): ColorInput => {
240
+ const colors: ColorInput = {};
241
+ for (const key of BASE_COLOR_KEYS) {
242
+ const value = params[`${key}${suffix}`];
243
+ if (value !== undefined) {
244
+ colors[key] = value;
245
+ }
246
+ }
247
+ return colors;
248
+ };
249
+
250
+ /**
251
+ * Every suffixed key, so asking "any per-scheme colors at all?" is one pass over names,
252
+ * not two objects built to be thrown away.
253
+ */
254
+ const MODE_OVERRIDE_KEYS: ReadonlyArray<keyof LightDarkColorParams> = THEME_VARIANTS.flatMap(
255
+ (variant) => BASE_COLOR_KEYS.map((key) => `${key}_${variant}` as const),
256
+ );
225
257
 
226
258
  /**
227
259
  * Returns resolved colors for both light and dark mode given all input params.
@@ -234,31 +266,22 @@ const extractLightDarkColors = (
234
266
  * Anything a mode does not override falls back to the general params,
235
267
  * so a partial override such as `bg_color_dark` alone keeps every other color from the base theme.
236
268
  *
237
- * When no `_light` / `_dark` param is provided at all, `darkColors` is `null` and the caller emits no dark-mode block.
269
+ * When no `_light` / `_dark` param is provided at all,
270
+ * `darkColors` is `null` and the caller emits no dark-mode block.
238
271
  *
239
- * @param params Raw query params, containing both general and `_light`/`_dark` suffixed colors and themes.
240
272
  * @returns `{ lightColors, darkColors }`, resolved colors for both light and dark mode
241
273
  */
242
274
  const getLightDarkColors = (
243
275
  params: ColorInput & LightDarkColorParams,
244
276
  ): { lightColors: CardColors; darkColors: CardColors | null } => {
245
- const lightOverrides = extractLightDarkColors(params, '_light');
246
- const darkOverrides = extractLightDarkColors(params, '_dark');
247
-
248
- const hasModeOverrides =
249
- Object.values(lightOverrides).some((v) => v !== undefined) ||
250
- Object.values(darkOverrides).some((v) => v !== undefined);
251
-
252
- if (!hasModeOverrides) {
277
+ // The common case is no per-scheme params at all, so it never builds the overrides.
278
+ if (!MODE_OVERRIDE_KEYS.some((key) => params[key] !== undefined)) {
253
279
  return { lightColors: getCardColors(params), darkColors: null };
254
280
  }
255
281
 
256
- const defined = (obj: ColorInput): ColorInput =>
257
- Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
258
-
259
282
  return {
260
- lightColors: getCardColors({ ...params, ...defined(lightOverrides) }),
261
- darkColors: getCardColors({ ...params, ...defined(darkOverrides) }),
283
+ lightColors: getCardColors({ ...params, ...extractLightDarkColors(params, '_light') }),
284
+ darkColors: getCardColors({ ...params, ...extractLightDarkColors(params, '_dark') }),
262
285
  };
263
286
  };
264
287
 
@@ -1,5 +1,5 @@
1
- import { defaultFetch } from './http.js';
2
- import type { FetchLike } from './http.js';
1
+ import { defaultFetch } from './http.ts';
2
+ import type { FetchLike } from './http.ts';
3
3
 
4
4
  type Env = Record<string, string | undefined>;
5
5
 
@@ -20,14 +20,12 @@ interface CardConfigInit {
20
20
  }
21
21
 
22
22
  /**
23
- * @param value Comma-separated string.
24
23
  * @returns Parsed string values.
25
24
  */
26
25
  const parseCsv = (value: string | undefined): Array<string> | undefined =>
27
26
  value ? value.split(',') : undefined;
28
27
 
29
28
  /**
30
- * @param value Raw `FETCH_MULTI_PAGE_STARS` value.
31
29
  * @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one.
32
30
  */
33
31
  const parseFetchMultiPageStars = (value: string | undefined): number => {
@@ -39,7 +37,6 @@ const parseFetchMultiPageStars = (value: string | undefined): number => {
39
37
  };
40
38
 
41
39
  /**
42
- * @param env Environment variables to inspect.
43
40
  * @returns Personal access tokens found in the environment.
44
41
  */
45
42
  const parsePATsFromEnv = (env: Env): Array<PersonalAccessToken> =>
@@ -74,10 +71,7 @@ export class CardConfig {
74
71
  this.fetch = init.fetch ?? defaultFetch;
75
72
  }
76
73
 
77
- /**
78
- * @param env Environment variables to read — the host passes `process.env` or an equivalent.
79
- * @returns Config for this deployment.
80
- */
74
+ /** @returns Config for this deployment. */
81
75
  static fromEnv(env: Env): CardConfig {
82
76
  return new CardConfig({
83
77
  pats: parsePATsFromEnv(env),
@@ -88,20 +82,13 @@ export class CardConfig {
88
82
  });
89
83
  }
90
84
 
91
- /**
92
- * @param id Username or gist id being requested.
93
- * @param kind Which allowlist applies.
94
- * @returns Whether this deployment serves the id.
95
- */
85
+ /** @returns Whether this deployment serves the id. */
96
86
  isAllowed(id: string, kind: 'username' | 'gist'): boolean {
97
87
  const list = kind === 'gist' ? this.gistAllowlist : this.usernameAllowlist;
98
88
  return list === undefined || list.includes(id);
99
89
  }
100
90
 
101
- /**
102
- * @param overrides Fields to replace.
103
- * @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request.
104
- */
91
+ /** @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request. */
105
92
  with(overrides: CardConfigInit): CardConfig {
106
93
  return new CardConfig({
107
94
  pats: this.pats,
@@ -1,4 +1,10 @@
1
- import type { RepositoryAffiliation } from '../graphql/generated/common.js';
1
+ import type { RepositoryAffiliation } from '../graphql/generated/common.ts';
2
+
3
+ /** The year GitHub launched, and so the earliest one a card can be asked to count from. */
4
+ const GITHUB_EPOCH_YEAR = 2008;
5
+
6
+ /** A GitHub login: alphanumerics and single inner hyphens, 39 characters at most. */
7
+ const GITHUB_USERNAME_PATTERN = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i;
2
8
 
3
9
  /**
4
10
  * Valid owner affiliations for GitHub API queries.
@@ -9,4 +15,4 @@ const OWNER_AFFILIATIONS: Array<RepositoryAffiliation> = [
9
15
  'ORGANIZATION_MEMBER',
10
16
  ];
11
17
 
12
- export { OWNER_AFFILIATIONS };
18
+ export { GITHUB_EPOCH_YEAR, GITHUB_USERNAME_PATTERN, OWNER_AFFILIATIONS };
@@ -1,4 +1,8 @@
1
- /** A span between two dates, as GitHub's range arguments take it. */
1
+ /** @file A range reaches GitHub one calendar year at a time; `contributionsCollection` refuses a longer one. */
2
+
3
+ import { GITHUB_EPOCH_YEAR } from './constants.ts';
4
+
5
+ /** A range between two dates, as GitHub's range arguments take it. */
2
6
  interface GitHubDateRange {
3
7
  /** Start of the range, inclusive. */
4
8
  from: Date;
@@ -6,23 +10,24 @@ interface GitHubDateRange {
6
10
  to: Date;
7
11
  }
8
12
 
13
+ /** A year, a year and month, or a full date. */
14
+ const RANGE_DATE_PATTERN = /^(?<year>\d{4})(?:-(?<month>\d{2})(?:-(?<day>\d{2}))?)?$/;
15
+
9
16
  /**
10
17
  * Format a date as a GitHub `DateTime` scalar.
11
18
  * Seconds precision, no milliseconds.
12
19
  *
13
- * @param date Date to format.
14
20
  * @returns e.g. `2024-01-01T00:00:00Z`.
15
21
  */
16
22
  const toGitHubDateTime = (date: Date): string => `${date.toISOString().slice(0, 19)}Z`;
17
23
 
18
24
  /**
19
- * The full UTC span of a calendar year, both ends inclusive.
25
+ * The full UTC range of a calendar year, both ends inclusive.
20
26
  *
21
27
  * The end matters to callers that would otherwise leave a range open:
22
28
  * GitHub's `contributionsCollection` defaults `to` to one year after `from`,
23
29
  * which pulls the next 1st of January into the year.
24
30
  *
25
- * @param year Four-digit year.
26
31
  * @returns The range covering it.
27
32
  */
28
33
  const getGitHubYearRange = (year: number): GitHubDateRange => ({
@@ -30,5 +35,130 @@ const getGitHubYearRange = (year: number): GitHubDateRange => ({
30
35
  to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)),
31
36
  });
32
37
 
33
- export { getGitHubYearRange, toGitHubDateTime };
38
+ /**
39
+ * The widest range a card can be asked for: GitHub's first year through the end of this one.
40
+ * The far end is the end of the year, not today, because a bare `to=2026` widens to its 31st of December.
41
+ *
42
+ * @returns The range, in UTC.
43
+ */
44
+ const getWidestRange = (): GitHubDateRange => ({
45
+ from: getGitHubYearRange(GITHUB_EPOCH_YEAR).from,
46
+ to: getGitHubYearRange(new Date().getUTCFullYear()).to,
47
+ });
48
+
49
+ /**
50
+ * The range a card counts within, from the ends the query named.
51
+ *
52
+ * @returns The range, each open end filled from {@link getWidestRange}.
53
+ */
54
+ const toRange = (from: Date | undefined, to: Date | undefined): GitHubDateRange => {
55
+ const widest = getWidestRange();
56
+ return { from: from ?? widest.from, to: to ?? widest.to };
57
+ };
58
+
59
+ /**
60
+ * Read one end of a range, written as `2024`, `2024-03` or `2024-03-15`.
61
+ * A partial date covers the whole of what it names, so `2024` starts on the 1st of January
62
+ * and ends on the 31st of December.
63
+ *
64
+ * @returns The instant, or `undefined` when the text does not name a date that exists.
65
+ */
66
+ const parseRangeDate = (value: string, end: 'from' | 'to'): Date | undefined => {
67
+ const match = RANGE_DATE_PATTERN.exec(value);
68
+ if (!match) {
69
+ return undefined;
70
+ }
71
+
72
+ const { year: yearText, month: monthText, day: dayText } = match.groups ?? {};
73
+ const year = Number(yearText);
74
+ const month = monthText === undefined ? 1 : Number(monthText);
75
+ const day = dayText === undefined ? 1 : Number(dayText);
76
+
77
+ const start = new Date(Date.UTC(year, month - 1, day));
78
+ // Date.UTC rolls an impossible date forward rather than refusing it
79
+ if (start.getUTCMonth() + 1 !== month || start.getUTCDate() !== day) {
80
+ return undefined;
81
+ }
82
+ if (end === 'from') {
83
+ return start;
84
+ }
85
+
86
+ const nextUnit =
87
+ dayText !== undefined
88
+ ? Date.UTC(year, month - 1, day + 1)
89
+ : monthText !== undefined
90
+ ? Date.UTC(year, month, 1)
91
+ : Date.UTC(year + 1, 0, 1);
92
+ return new Date(nextUnit - 1000);
93
+ };
94
+
95
+ /**
96
+ * Split a range into one per calendar year, each clipped to it.
97
+ *
98
+ * @returns Them, oldest first; empty when the range is inverted.
99
+ */
100
+ const toYearRanges = ({ from, to }: GitHubDateRange): Array<GitHubDateRange> => {
101
+ const ranges: Array<GitHubDateRange> = [];
102
+ for (let year = from.getUTCFullYear(); year <= to.getUTCFullYear(); year += 1) {
103
+ const yearRange = getGitHubYearRange(year);
104
+ const start = from > yearRange.from ? from : yearRange.from;
105
+ const end = to < yearRange.to ? to : yearRange.to;
106
+ if (start <= end) {
107
+ ranges.push({ from: start, to: end });
108
+ }
109
+ }
110
+ return ranges;
111
+ };
112
+
113
+ /**
114
+ * Whole years as a card writes them.
115
+ *
116
+ * @returns e.g. `2024` or `2020–2024`.
117
+ */
118
+ const formatYears = (first: number, last: number): string =>
119
+ first === last ? `${first}` : `${first}–${last}`;
120
+
121
+ /**
122
+ * A range as a card writes it, at the coarsest granularity that stays true.
123
+ *
124
+ * @returns e.g. `2024`, `2020–2024` or `2024-03-15 – 2024-06-30`.
125
+ */
126
+ const formatRange = ({ from, to }: GitHubDateRange): string => {
127
+ const firstYear = from.getUTCFullYear();
128
+ const lastYear = to.getUTCFullYear();
129
+ const wholeYears =
130
+ from.getTime() === getGitHubYearRange(firstYear).from.getTime() &&
131
+ to.getTime() === getGitHubYearRange(lastYear).to.getTime();
132
+
133
+ if (!wholeYears) {
134
+ return `${from.toISOString().slice(0, 10)} – ${to.toISOString().slice(0, 10)}`;
135
+ }
136
+ return formatYears(firstYear, lastYear);
137
+ };
138
+
139
+ /**
140
+ * The ranges a walk asks GitHub for: one per calendar year the account contributed in.
141
+ * Ordered here, which is what makes a walk's ties chronological.
142
+ *
143
+ * @returns Them, oldest first.
144
+ */
145
+ const toContributionRanges = (
146
+ contributionYears: ReadonlyArray<number>,
147
+ range: GitHubDateRange,
148
+ ): Array<GitHubDateRange> => {
149
+ const contributed = new Set(contributionYears);
150
+ return toYearRanges(range).filter((year) => contributed.has(year.from.getUTCFullYear()));
151
+ };
152
+
153
+ export {
154
+ formatRange,
155
+ formatYears,
156
+ getWidestRange,
157
+ getGitHubYearRange,
158
+ parseRangeDate,
159
+ toContributionRanges,
160
+ toGitHubDateTime,
161
+ toRange,
162
+ toYearRanges,
163
+ };
34
164
  export type { GitHubDateRange };