@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
package/src/cards/gist.ts CHANGED
@@ -1,33 +1,39 @@
1
- import { default as Card } from '../common/Card.js';
2
- import { getLightDarkColors } from '../common/color.js';
3
- import { kFormatter, wrapTextMultiline } from '../common/fmt.js';
4
- import { encodeHTML } from '../common/html.js';
5
- import { icons } from '../common/icons.js';
6
- import { getLanguageColor } from '../common/languageColors.js';
7
- import { parseEmojis } from '../common/ops.js';
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, font } from '../common/brand.ts';
2
+ import { Card } from '../common/Card.ts';
3
+ import { getLightDarkColors } from '../common/color.ts';
4
+ import { kFormatter } from '../common/fmt.ts';
5
+ import { I18n } from '../common/I18n.ts';
6
+ import { icons } from '../common/icons.ts';
7
+ import { getLanguageColor } from '../common/languageColors.ts';
8
+ import { parseEmojis } from '../common/ops.ts';
8
9
  import {
9
10
  countWrappedLines,
11
+ wrapTextMultiline,
10
12
  createLanguageNode,
11
13
  flexLayout,
12
14
  iconWithLabel,
13
15
  measureText,
14
16
  wrappedTextNode,
15
17
  wrappedTextStyles,
16
- } from '../common/render.js';
17
- import type { GistData } from '../fetchers/types.js';
18
+ } from '../common/render.ts';
19
+ import type { GistData } from '../fetchers/types.ts';
20
+ import type { Child } from '../markup/index.ts';
21
+ import { el, rule } from '../markup/index.ts';
22
+ import { gistCardLocales } from '../translations.ts';
18
23
 
19
- import type { CardOptions, CommonCardOptions } from './options.js';
24
+ import type { CardOptions, CommonCardOptions } from './options.ts';
20
25
 
21
26
  const ICON_SIZE = 16;
22
- const CARD_DEFAULT_WIDTH = 400;
27
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
23
28
  const X_OFFSET = 25;
24
29
  const HEADER_MAX_LENGTH = 35;
25
30
  const DESCRIPTION_BOX_WIDTH = CARD_DEFAULT_WIDTH - 2 * X_OFFSET;
26
- const DESCRIPTION_FONT_SIZE = 13;
31
+ const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
27
32
  const DESCRIPTION_LINE_HEIGHT_PX = 16;
28
33
  const DESCRIPTION_MAX_LINES = 10;
29
34
 
30
35
  interface GistCardOptions extends CommonCardOptions {
36
+ locale: string;
31
37
  show_owner: boolean;
32
38
  browser_rendering: boolean;
33
39
  }
@@ -35,13 +41,12 @@ interface GistCardOptions extends CommonCardOptions {
35
41
  /**
36
42
  * Render gist card.
37
43
  *
38
- * @param gistData Gist data.
39
- * @param options Gist card options.
40
44
  * @returns Gist card.
41
45
  */
42
46
  const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions> = {}): string => {
43
47
  const { name, nameWithOwner, description, language, starsCount, forksCount } = gistData;
44
48
  const {
49
+ locale,
45
50
  theme = 'default_repocard',
46
51
  border_radius,
47
52
  show_owner = false,
@@ -51,10 +56,12 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
51
56
 
52
57
  const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
53
58
 
54
- const desc = parseEmojis(description || 'No description provided');
59
+ const i18n = new I18n({ locale, translations: gistCardLocales });
60
+
61
+ const desc = parseEmojis(description || i18n.t('gistcard.no-description'));
55
62
 
56
63
  let descriptionLines: number;
57
- let descriptionSvg: string;
64
+ let descriptionSvg: Child;
58
65
  if (browser_rendering) {
59
66
  // The browser performs the actual text wrapping inside the foreignObject;
60
67
  // we only estimate the line count server-side so the SVG can reserve enough
@@ -86,12 +93,11 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
86
93
  linesLimit,
87
94
  );
88
95
  descriptionLines = multiLineDescription.length;
89
- descriptionSvg = multiLineDescription
90
- .map((line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`)
91
- .join('');
92
- descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
93
- ${descriptionSvg}
94
- </text>`;
96
+ descriptionSvg = el(
97
+ 'text',
98
+ { class: 'description', x: X_OFFSET, y: -5 },
99
+ multiLineDescription.map((line) => el('tspan', { dy: '1.2em', x: X_OFFSET }, line)),
100
+ );
95
101
  }
96
102
 
97
103
  const lineHeight = descriptionLines > 3 ? 12 : 10;
@@ -102,7 +108,7 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
102
108
  const svgStars = iconWithLabel(icons.star, totalStars, 'starsCount', ICON_SIZE);
103
109
  const svgForks = iconWithLabel(icons.fork, totalForks, 'forksCount', ICON_SIZE);
104
110
 
105
- const languageName = language || 'Unspecified';
111
+ const languageName = language || i18n.t('gistcard.unspecified-language');
106
112
  const languageColor = getLanguageColor(languageName);
107
113
 
108
114
  const svgLanguage = createLanguageNode(languageName, languageColor);
@@ -115,14 +121,14 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
115
121
  ICON_SIZE + measureText(`${totalForks}`, 12),
116
122
  ],
117
123
  gap: 25,
118
- }).join('');
124
+ });
119
125
 
120
126
  const header = show_owner ? nameWithOwner : name;
121
127
 
122
128
  const card = new Card({
123
129
  defaultTitle:
124
130
  header.length > HEADER_MAX_LENGTH ? `${header.slice(0, HEADER_MAX_LENGTH)}...` : header,
125
- titlePrefixIcon: icons.gist,
131
+ titlePrefixIcon: CARD_ICON.gist,
126
132
  width: CARD_DEFAULT_WIDTH,
127
133
  height,
128
134
  border_radius,
@@ -130,22 +136,23 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
130
136
  });
131
137
 
132
138
  card.setCSS({
133
- light: ({ textColor, iconColor }) => `
134
- .description {
135
- font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor};
136
- ${browser_rendering ? wrappedTextStyles(textColor) : ''}
137
- }
138
- .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
139
- .icon { fill: ${iconColor} }
140
- `,
141
- dark: ({ textColor, iconColor }) => `
142
- .description {
143
- fill: ${textColor};
144
- ${browser_rendering ? wrappedTextStyles(textColor) : ''}
145
- }
146
- .gray { fill: ${textColor} }
147
- .icon { fill: ${iconColor} }
148
- `,
139
+ light: ({ textColor, iconColor }) => [
140
+ rule('.description', {
141
+ font: font('regular', 'meta'),
142
+ fill: textColor,
143
+ ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
144
+ }),
145
+ rule('.gray', { font: font('regular', 'small'), fill: textColor }),
146
+ rule('.icon', { fill: iconColor }),
147
+ ],
148
+ dark: ({ textColor, iconColor }) => [
149
+ rule('.description', {
150
+ fill: textColor,
151
+ ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
152
+ }),
153
+ rule('.gray', { fill: textColor }),
154
+ rule('.icon', { fill: iconColor }),
155
+ ],
149
156
  });
150
157
 
151
158
  card.setHideBorder(hide_border);
@@ -154,16 +161,15 @@ const renderGistCard = (gistData: GistData, options: CardOptions<GistCardOptions
154
161
  // shows has to be repeated here.
155
162
  card.setAccessibilityLabel({
156
163
  title: card.title,
157
- desc: `${desc}. Language: ${languageName}, Stars: ${totalStars}, Forks: ${totalForks}`,
164
+ desc: `${desc}. ${i18n.t('gistcard.language')}: ${languageName}, ${i18n.t(
165
+ 'gistcard.stars',
166
+ )}: ${totalStars}, ${i18n.t('gistcard.forks')}: ${totalForks}`,
158
167
  });
159
168
 
160
- return card.render(`
161
- ${descriptionSvg}
162
-
163
- <g transform="translate(30, ${height - 75})">
164
- ${starAndForkCount}
165
- </g>
166
- `);
169
+ return card.render([
170
+ descriptionSvg,
171
+ el('g', { transform: `translate(30, ${height - 75})` }, starAndForkCount),
172
+ ]);
167
173
  };
168
174
 
169
175
  export { renderGistCard };
@@ -1,4 +1,4 @@
1
- import type { ColorParams } from '../common/color.js';
1
+ import type { ColorParams } from '../common/color.ts';
2
2
 
3
3
  /**
4
4
  * Options every card accepts.
package/src/cards/repo.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { Card } from '../common/Card.js';
2
- import { getLightDarkColors } from '../common/color.js';
3
- import { kFormatter, wrapTextMultiline } from '../common/fmt.js';
4
- import { encodeHTML } from '../common/html.js';
5
- import { I18n } from '../common/I18n.js';
6
- import { icons } from '../common/icons.js';
7
- import { buildSearchFilter, clampValue, parseEmojis } from '../common/ops.js';
1
+ import { CARD_ICON, CARD_WIDTH, FONT_SIZE, FONT_WEIGHT, font } from '../common/brand.ts';
2
+ import { Card } from '../common/Card.ts';
3
+ import { getLightDarkColors } from '../common/color.ts';
4
+ import { kFormatter } from '../common/fmt.ts';
5
+ import { I18n } from '../common/I18n.ts';
6
+ import { icons } from '../common/icons.ts';
7
+ import { buildSearchFilter, clampValue, parseEmojis } from '../common/ops.ts';
8
8
  import {
9
+ NUMBER_FORMATS,
9
10
  countWrappedLines,
11
+ wrapTextMultiline,
10
12
  createLanguageNode,
11
13
  createTextNode,
12
14
  flexLayout,
@@ -14,19 +16,31 @@ import {
14
16
  measureText,
15
17
  wrappedTextNode,
16
18
  wrappedTextStyles,
17
- } from '../common/render.js';
18
- import type { RepositoryData } from '../fetchers/types.js';
19
- import { repoCardLocales } from '../translations.js';
19
+ } from '../common/render.ts';
20
+ import type { RepositoryData } from '../fetchers/types.ts';
21
+ import type { Child, MarkupElement } from '../markup/index.ts';
22
+ import { el, rule } from '../markup/index.ts';
23
+ import { repoCardLocales } from '../translations.ts';
20
24
 
21
- import type { CardOptions, CommonCardOptions } from './options.js';
25
+ import type { CardOptions, CommonCardOptions } from './options.ts';
22
26
 
23
27
  const ICON_SIZE = 16;
24
- const CARD_DEFAULT_WIDTH = 400;
28
+ const CARD_DEFAULT_WIDTH = CARD_WIDTH.standard;
25
29
  const X_OFFSET = 25;
26
- const DESCRIPTION_FONT_SIZE = 13;
30
+ const DESCRIPTION_FONT_SIZE = FONT_SIZE.meta;
27
31
  const DESCRIPTION_LINE_HEIGHT_PX = 16;
28
32
  const DESCRIPTION_MAX_LINES = 3;
29
33
 
34
+ /** Stats the card draws only when `show` names them. */
35
+ const REPO_SHOW_STATS = [
36
+ 'prs_authored',
37
+ 'prs_commented',
38
+ 'prs_reviewed',
39
+ 'issues_authored',
40
+ 'issues_commented',
41
+ ] as const;
42
+ type RepoShowStat = (typeof REPO_SHOW_STATS)[number];
43
+
30
44
  interface RepoCardOptions extends CommonCardOptions {
31
45
  locale: string;
32
46
  show_owner: boolean;
@@ -42,7 +56,7 @@ interface RepoCardOptions extends CommonCardOptions {
42
56
  }
43
57
 
44
58
  interface RepoStatItem {
45
- icon: string;
59
+ icon: Child;
46
60
  label: string;
47
61
  value: number | undefined;
48
62
  id: string;
@@ -53,41 +67,37 @@ interface RepoStatItem {
53
67
  /**
54
68
  * Retrieves the repository description and wraps it to fit the card width.
55
69
  *
56
- * @param label The repository description.
57
- * @param xOffset Horizontal offset of the badge.
58
70
  * @returns Wrapped repo description SVG object.
59
71
  */
60
- const getBadgeSVG = (label: string, xOffset = 0): string => {
72
+ const getBadgeSVG = (label: string, xOffset = 0): MarkupElement => {
61
73
  if (!Number.isFinite(xOffset)) {
62
- throw new Error(`Invalid xOffset: "${xOffset}"`);
74
+ throw new TypeError(`Invalid xOffset: "${xOffset}"`);
63
75
  }
64
76
 
65
- return `
66
- <g data-testid="badge" class="badge" transform="translate(${320 + xOffset}, -18)">
67
- <rect stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
68
- <text
69
- x="23" y="-5"
70
- alignment-baseline="central"
71
- dominant-baseline="central"
72
- text-anchor="middle"
73
- >
74
- ${encodeHTML(label)}
75
- </text>
76
- </g>
77
- `;
77
+ return el(
78
+ 'g',
79
+ { 'data-testid': 'badge', class: 'badge', transform: `translate(${320 + xOffset}, -18)` },
80
+ el('rect', { 'stroke-width': 1, width: 70, height: 20, x: -12, y: -14, ry: 10, rx: 10 }),
81
+ el(
82
+ 'text',
83
+ {
84
+ x: 23,
85
+ y: -5,
86
+ 'alignment-baseline': 'central',
87
+ 'dominant-baseline': 'central',
88
+ 'text-anchor': 'middle',
89
+ },
90
+ label,
91
+ ),
92
+ );
78
93
  };
79
94
 
80
95
  /**
81
96
  * Renders repository card details.
82
97
  *
83
- * @param repo Repository data.
84
- * @param options Card options.
85
98
  * @returns Repository card SVG object.
86
99
  */
87
- const renderRepoCard = (
88
- repo: RepositoryData,
89
- options: CardOptions<RepoCardOptions> = {},
90
- ): string => {
100
+ const renderCard = (repo: RepositoryData, options: CardOptions<RepoCardOptions> = {}): string => {
91
101
  const {
92
102
  name,
93
103
  nameWithOwner,
@@ -121,7 +131,7 @@ const renderRepoCard = (
121
131
  } = options;
122
132
 
123
133
  const card_width =
124
- card_width_input && !isNaN(card_width_input)
134
+ card_width_input && !Number.isNaN(card_width_input)
125
135
  ? card_width_input
126
136
  : show.length >= 2
127
137
  ? CARD_DEFAULT_WIDTH + 30
@@ -132,10 +142,13 @@ const renderRepoCard = (
132
142
  translations: repoCardLocales,
133
143
  });
134
144
 
145
+ // Typed against the exported list, so a stat drawn here cannot be missing from it.
146
+ const shows = (stat: RepoShowStat): boolean => show.includes(stat);
147
+
135
148
  const repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
136
149
  const encodedUsername = encodeURIComponent(username ?? '');
137
150
  const STATS: Record<string, RepoStatItem> = {};
138
- if (show.includes('prs_authored')) {
151
+ if (shows('prs_authored')) {
139
152
  STATS['prs_authored'] = {
140
153
  icon: icons.prs,
141
154
  label: i18n.t('repocard.prs-authored'),
@@ -144,7 +157,7 @@ const renderRepoCard = (
144
157
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=pullrequests`,
145
158
  };
146
159
  }
147
- if (show.includes('prs_commented')) {
160
+ if (shows('prs_commented')) {
148
161
  STATS['prs_commented'] = {
149
162
  icon: icons.comments,
150
163
  label: i18n.t('repocard.prs-commented'),
@@ -153,7 +166,7 @@ const renderRepoCard = (
153
166
  link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
154
167
  };
155
168
  }
156
- if (show.includes('prs_reviewed')) {
169
+ if (shows('prs_reviewed')) {
157
170
  STATS['prs_reviewed'] = {
158
171
  icon: icons.reviews,
159
172
  label: i18n.t('repocard.prs-reviewed'),
@@ -162,7 +175,7 @@ const renderRepoCard = (
162
175
  link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&amp;type=pullrequests`,
163
176
  };
164
177
  }
165
- if (show.includes('issues_authored')) {
178
+ if (shows('issues_authored')) {
166
179
  STATS['issues_authored'] = {
167
180
  icon: icons.issues,
168
181
  label: i18n.t('repocard.issues-authored'),
@@ -171,7 +184,7 @@ const renderRepoCard = (
171
184
  link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&amp;type=issues`,
172
185
  };
173
186
  }
174
- if (show.includes('issues_commented')) {
187
+ if (shows('issues_commented')) {
175
188
  STATS['issues_commented'] = {
176
189
  icon: icons.discussions_started,
177
190
  label: i18n.t('repocard.issues-commented'),
@@ -199,16 +212,16 @@ const renderRepoCard = (
199
212
  }),
200
213
  );
201
214
 
202
- const extraLHeight = parseInt(String(line_height), 10);
215
+ const extraLHeight = Number.parseInt(String(line_height), 10);
203
216
  const lineHeight = 10;
204
217
  const header = show_owner ? nameWithOwner : name;
205
- const langName = (primaryLanguage && primaryLanguage.name) || 'Unspecified';
206
- const langColor = (primaryLanguage && primaryLanguage.color) || '#333';
207
- const desc = parseEmojis(description || 'No description provided');
218
+ const langName = primaryLanguage?.name || i18n.t('repocard.unspecified-language');
219
+ const langColor = primaryLanguage?.color || '#333';
220
+ const desc = parseEmojis(description || i18n.t('repocard.no-description'));
208
221
  const descriptionBoxWidth = card_width - 2 * X_OFFSET;
209
222
 
210
223
  let descriptionLinesCount: number;
211
- let descriptionSvg: string;
224
+ let descriptionSvg: Child;
212
225
  if (browser_rendering) {
213
226
  // The browser performs the actual text wrapping inside the foreignObject;
214
227
  // we only estimate the line count server-side so the SVG can reserve enough
@@ -240,23 +253,21 @@ const renderRepoCard = (
240
253
  descriptionLinesCount = description_lines_count
241
254
  ? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
242
255
  : multiLineDescription.length;
243
- descriptionSvg = multiLineDescription
244
- .map((line) => `<tspan dy="1.2em" x="${X_OFFSET}">${encodeHTML(line)}</tspan>`)
245
- .join('');
246
- descriptionSvg = `<text class="description" x="${X_OFFSET}" y="-5">
247
- ${descriptionSvg}
248
- </text>`;
256
+ descriptionSvg = el(
257
+ 'text',
258
+ { class: 'description', x: X_OFFSET, y: -5 },
259
+ multiLineDescription.map((line) => el('tspan', { dy: '1.2em', x: X_OFFSET }, line)),
260
+ );
249
261
  }
250
262
 
251
- const extraHeight = Object.keys(STATS).length
252
- ? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight
253
- : 0;
263
+ const extraHeight =
264
+ Object.keys(STATS).length > 0 ? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight : 0;
254
265
  const height =
255
266
  (descriptionLinesCount > 1 ? 120 : 110) + descriptionLinesCount * lineHeight + extraHeight;
256
267
 
257
268
  const { lightColors, darkColors } = getLightDarkColors({ ...options, theme });
258
269
 
259
- const svgLanguage = primaryLanguage ? createLanguageNode(langName, langColor) : '';
270
+ const svgLanguage = primaryLanguage ? createLanguageNode(langName, langColor) : undefined;
260
271
 
261
272
  const totalStars = kFormatter(stargazerCount);
262
273
  const totalForks = kFormatter(forkCount);
@@ -271,31 +282,31 @@ const renderRepoCard = (
271
282
  ICON_SIZE + measureText(`${totalForks}`, 12),
272
283
  ],
273
284
  gap: 25,
274
- }).join('');
285
+ });
275
286
 
276
- const extraRows: Array<string> = [];
287
+ const extraRows: Array<Child> = [];
277
288
  for (let i = 0; i < statItems.length; i += 2) {
278
289
  extraRows.push(
279
290
  flexLayout({
280
291
  items: statItems.slice(i, i + 2),
281
292
  gap: 210,
282
293
  direction: 'row',
283
- }).join(''),
294
+ }),
284
295
  );
285
296
  }
286
- const extraItems = `
287
- <svg x="0" y="0"><g transform="translate(-3, ${height - 52 - extraHeight})">
288
- ${flexLayout({
289
- items: extraRows,
290
- gap: extraLHeight,
291
- direction: 'column',
292
- }).join('')}
293
- </g></svg>
294
- `;
297
+ const extraItems = el(
298
+ 'svg',
299
+ { x: 0, y: 0 },
300
+ el(
301
+ 'g',
302
+ { transform: `translate(-3, ${height - 52 - extraHeight})` },
303
+ flexLayout({ items: extraRows, gap: extraLHeight, direction: 'column' }),
304
+ ),
305
+ );
295
306
 
296
307
  const card = new Card({
297
308
  defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
298
- titlePrefixIcon: icons.contribs,
309
+ titlePrefixIcon: CARD_ICON.repo,
299
310
  width: card_width,
300
311
  height,
301
312
  border_radius,
@@ -306,39 +317,36 @@ const renderRepoCard = (
306
317
  card.setHideBorder(hide_border);
307
318
  card.setHideTitle(false);
308
319
  card.setCSS({
309
- light: ({ textColor, iconColor }) => `
310
- .description {
311
- font: 400 ${DESCRIPTION_FONT_SIZE}px 'Segoe UI', Ubuntu, Sans-Serif;fill: ${textColor};
312
- ${browser_rendering ? wrappedTextStyles(textColor) : ''}
313
- }
314
- .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
315
- .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
316
- .badge rect { opacity: 0.2; stroke: ${textColor} }
317
- .badge text { fill: ${textColor} }
318
-
319
- .stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
320
- .stagger {
321
- opacity: 0;
322
- animation: fadeInAnimation 0.3s ease-in-out forwards;
323
- }
324
- .not_bold { font-weight: 400 }
325
- .bold { font-weight: 700 }
326
- .icon {
327
- fill: ${iconColor};
328
- display: block;
329
- }
330
- `,
331
- dark: ({ textColor, iconColor }) => `
332
- .description {
333
- fill: ${textColor};
334
- ${browser_rendering ? wrappedTextStyles(textColor) : ''}
335
- }
336
- .gray { fill: ${textColor} }
337
- .badge rect { stroke: ${textColor} }
338
- .badge text { fill: ${textColor} }
339
- .stat { fill: ${textColor} }
340
- .icon { fill: ${iconColor}; }
341
- `,
320
+ light: ({ textColor, iconColor }) => [
321
+ rule('.description', {
322
+ font: font('regular', 'meta'),
323
+ fill: textColor,
324
+ ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
325
+ }),
326
+ rule('.gray', { font: font('regular', 'small'), fill: textColor }),
327
+ rule('.badge', { font: font('semibold', 'micro') }),
328
+ rule('.badge rect', { opacity: 0.2, stroke: textColor }),
329
+ rule('.badge text', { fill: textColor }),
330
+ rule('.stat', { font: font('regular', 'small'), fill: textColor }),
331
+ rule('.stagger', {
332
+ opacity: 0,
333
+ animation: 'fadeInAnimation 0.3s ease-in-out forwards',
334
+ }),
335
+ rule('.not_bold', { 'font-weight': FONT_WEIGHT.regular }),
336
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
337
+ rule('.icon', { fill: iconColor, display: 'block' }),
338
+ ],
339
+ dark: ({ textColor, iconColor }) => [
340
+ rule('.description', {
341
+ fill: textColor,
342
+ ...(browser_rendering ? wrappedTextStyles(textColor) : {}),
343
+ }),
344
+ rule('.gray', { fill: textColor }),
345
+ rule('.badge rect', { stroke: textColor }),
346
+ rule('.badge text', { fill: textColor }),
347
+ rule('.stat', { fill: textColor }),
348
+ rule('.icon', { fill: iconColor }),
349
+ ],
342
350
  });
343
351
 
344
352
  const extraStatLabels = Object.values(STATS)
@@ -355,22 +363,28 @@ const renderRepoCard = (
355
363
  desc: [`${desc}.`, primaryLanguage ? langName : '', extraStatLabels].filter(Boolean).join(', '),
356
364
  });
357
365
 
358
- return card.render(`
359
- ${
360
- isTemplate
361
- ? getBadgeSVG(i18n.t('repocard.template'), card_width - CARD_DEFAULT_WIDTH)
362
- : isArchived
363
- ? getBadgeSVG(i18n.t('repocard.archived'), card_width - CARD_DEFAULT_WIDTH)
364
- : ''
365
- }
366
-
367
- ${descriptionSvg}
368
-
369
- <g transform="translate(30, ${height - 75 - extraHeight})">
370
- ${starAndForkCount}
371
- </g>
372
- ${extraItems}
373
- `);
366
+ return card.render([
367
+ isTemplate
368
+ ? getBadgeSVG(i18n.t('repocard.template'), card_width - CARD_DEFAULT_WIDTH)
369
+ : isArchived
370
+ ? getBadgeSVG(i18n.t('repocard.archived'), card_width - CARD_DEFAULT_WIDTH)
371
+ : undefined,
372
+ descriptionSvg,
373
+ el('g', { transform: `translate(30, ${height - 75 - extraHeight})` }, starAndForkCount),
374
+ extraItems,
375
+ ]);
374
376
  };
375
377
 
378
+ /**
379
+ * The card, and the values each of its options accepts, keyed by the option's own name.
380
+ * They ride on the renderer so a list cannot be found without what draws it;
381
+ * the api handler forwards them onto its own export, which is what a UI reads.
382
+ */
383
+ const renderRepoCard = Object.assign(renderCard, {
384
+ OPTIONS: {
385
+ show: REPO_SHOW_STATS,
386
+ number_format: NUMBER_FORMATS,
387
+ },
388
+ });
389
+
376
390
  export { renderRepoCard };