@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,17 +1,19 @@
1
- import { Card } from '../common/Card.js';
2
- import { getLightDarkColors, isPrefixedHexColor } from '../common/color.js';
3
- import { formatBytes } from '../common/fmt.js';
4
- import { encodeHTML } from '../common/html.js';
5
- import { I18n } from '../common/I18n.js';
6
- import { DEFAULT_LANG_COLOR } from '../common/languageColors.js';
7
- import { chunkArray, clampValue, lowercaseTrim } from '../common/ops.js';
8
- import { createProgressNode, flexLayout, measureText } from '../common/render.js';
9
- import type { Lang, TopLangData } from '../fetchers/types.js';
10
- import { langCardLocales } from '../translations.js';
11
-
12
- import type { CardOptions, CommonCardOptions } from './options.js';
13
-
14
- const DEFAULT_CARD_WIDTH = 300;
1
+ import { CARD_ICON, CARD_WIDTH, FONT_WEIGHT, firefoxFontSize, font } from '../common/brand.ts';
2
+ import { Card } from '../common/Card.ts';
3
+ import { getLightDarkColors, isPrefixedHexColor } from '../common/color.ts';
4
+ import { formatBytes } from '../common/fmt.ts';
5
+ import { I18n } from '../common/I18n.ts';
6
+ import { DEFAULT_LANG_COLOR } from '../common/languageColors.ts';
7
+ import { chunkArray, clampValue, lowercaseTrim } from '../common/ops.ts';
8
+ import { createProgressNode, flexLayout, measureText } from '../common/render.ts';
9
+ import type { Lang, TopLangData } from '../fetchers/types.ts';
10
+ import type { Child, MarkupElement } from '../markup/index.ts';
11
+ import { atRule, el, rule } from '../markup/index.ts';
12
+ import { langCardLocales } from '../translations.ts';
13
+
14
+ import type { CardOptions, CommonCardOptions } from './options.ts';
15
+
16
+ const DEFAULT_CARD_WIDTH = CARD_WIDTH.compact;
15
17
  const MIN_CARD_WIDTH = 280;
16
18
  const CARD_PADDING = 25;
17
19
  const COMPACT_LAYOUT_BASE_HEIGHT = 90;
@@ -20,6 +22,8 @@ const MAXIMUM_LANGS_COUNT = 20;
20
22
  const NORMAL_LAYOUT_DEFAULT_LANGS_COUNT = 5;
21
23
  const COMPACT_LAYOUT_DEFAULT_LANGS_COUNT = 6;
22
24
  const DONUT_LAYOUT_DEFAULT_LANGS_COUNT = 5;
25
+ /** Room for the chart that sits beside the language list. */
26
+ const DONUT_EXTRA_WIDTH = 50;
23
27
  const PIE_LAYOUT_DEFAULT_LANGS_COUNT = 6;
24
28
  const DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT = 6;
25
29
 
@@ -49,38 +53,35 @@ interface TopLangOptions extends CommonCardOptions {
49
53
  /**
50
54
  * Retrieves the programming language whose name is the longest.
51
55
  *
52
- * @param arr Array of programming languages.
53
56
  * @returns Longest programming language object.
54
57
  */
55
- const getLongestLang = (arr: Array<Lang>): Pick<Lang, 'name' | 'size' | 'color'> =>
56
- arr.reduce<Pick<Lang, 'name' | 'size' | 'color'>>(
57
- (savedLang, lang) => (lang.name.length > savedLang.name.length ? lang : savedLang),
58
- { name: '', size: 0, color: '' },
59
- );
58
+ const getLongestLang = (arr: Array<Lang>): Pick<Lang, 'name' | 'size' | 'color'> => {
59
+ let longest: Pick<Lang, 'name' | 'size' | 'color'> = { name: '', size: 0, color: '' };
60
+ for (const lang of arr) {
61
+ if (lang.name.length > longest.name.length) {
62
+ longest = lang;
63
+ }
64
+ }
65
+ return longest;
66
+ };
60
67
 
61
68
  /**
62
69
  * Convert degrees to radians.
63
70
  *
64
- * @param angleInDegrees Angle in degrees.
65
71
  * @returns Angle in radians.
66
72
  */
67
- const degreesToRadians = (angleInDegrees: number): number => angleInDegrees * (Math.PI / 180.0);
73
+ const degreesToRadians = (angleInDegrees: number): number => angleInDegrees * (Math.PI / 180);
68
74
 
69
75
  /**
70
76
  * Convert radians to degrees.
71
77
  *
72
- * @param angleInRadians Angle in radians.
73
78
  * @returns Angle in degrees.
74
79
  */
75
- const radiansToDegrees = (angleInRadians: number): number => angleInRadians / (Math.PI / 180.0);
80
+ const radiansToDegrees = (angleInRadians: number): number => angleInRadians / (Math.PI / 180);
76
81
 
77
82
  /**
78
83
  * Convert polar coordinates to cartesian coordinates.
79
84
  *
80
- * @param centerX Center x coordinate.
81
- * @param centerY Center y coordinate.
82
- * @param radius Radius of the circle.
83
- * @param angleInDegrees Angle in degrees.
84
85
  * @returns Cartesian coordinates.
85
86
  */
86
87
  const polarToCartesian = (
@@ -99,10 +100,6 @@ const polarToCartesian = (
99
100
  /**
100
101
  * Convert cartesian coordinates to polar coordinates.
101
102
  *
102
- * @param centerX Center x coordinate.
103
- * @param centerY Center y coordinate.
104
- * @param x Point x coordinate.
105
- * @param y Point y coordinate.
106
103
  * @returns Polar coordinates.
107
104
  */
108
105
  const cartesianToPolar = (
@@ -111,7 +108,7 @@ const cartesianToPolar = (
111
108
  x: number,
112
109
  y: number,
113
110
  ): { radius: number; angleInDegrees: number } => {
114
- const radius = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
111
+ const radius = Math.hypot(x - centerX, y - centerY);
115
112
  let angleInDegrees = radiansToDegrees(Math.atan2(y - centerY, x - centerX));
116
113
  if (angleInDegrees < 0) {
117
114
  angleInDegrees += 360;
@@ -122,79 +119,60 @@ const cartesianToPolar = (
122
119
  /**
123
120
  * Calculates length of circle.
124
121
  *
125
- * @param radius Radius of the circle.
126
122
  * @returns The length of the circle.
127
123
  */
128
- const getCircleLength = (radius: number): number => {
129
- return 2 * Math.PI * radius;
130
- };
124
+ const getCircleLength = (radius: number): number => 2 * Math.PI * radius;
131
125
 
132
126
  /**
133
127
  * Calculates height for the compact layout.
134
128
  *
135
- * @param totalLangs Total number of languages.
136
129
  * @returns Card height.
137
130
  */
138
- const calculateCompactLayoutHeight = (totalLangs: number): number => {
139
- return COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25;
140
- };
131
+ const calculateCompactLayoutHeight = (totalLangs: number): number =>
132
+ COMPACT_LAYOUT_BASE_HEIGHT + Math.round(totalLangs / 2) * 25;
141
133
 
142
134
  /**
143
135
  * Calculates height for the normal layout.
144
136
  *
145
- * @param totalLangs Total number of languages.
146
137
  * @returns Card height.
147
138
  */
148
- const calculateNormalLayoutHeight = (totalLangs: number): number => {
149
- return 45 + (totalLangs + 1) * 40;
150
- };
139
+ const calculateNormalLayoutHeight = (totalLangs: number): number => 45 + (totalLangs + 1) * 40;
151
140
 
152
141
  /**
153
142
  * Calculates height for the donut layout.
154
143
  *
155
- * @param totalLangs Total number of languages.
156
144
  * @returns Card height.
157
145
  */
158
- const calculateDonutLayoutHeight = (totalLangs: number): number => {
159
- return 215 + Math.max(totalLangs - 5, 0) * 32;
160
- };
146
+ const calculateDonutLayoutHeight = (totalLangs: number): number =>
147
+ 215 + Math.max(totalLangs - 5, 0) * 32;
161
148
 
162
149
  /**
163
150
  * Calculates height for the donut vertical layout.
164
151
  *
165
- * @param totalLangs Total number of languages.
166
152
  * @returns Card height.
167
153
  */
168
- const calculateDonutVerticalLayoutHeight = (totalLangs: number): number => {
169
- return 300 + Math.round(totalLangs / 2) * 25;
170
- };
154
+ const calculateDonutVerticalLayoutHeight = (totalLangs: number): number =>
155
+ 300 + Math.round(totalLangs / 2) * 25;
171
156
 
172
157
  /**
173
158
  * Calculates height for the pie layout.
174
159
  *
175
- * @param totalLangs Total number of languages.
176
160
  * @returns Card height.
177
161
  */
178
- const calculatePieLayoutHeight = (totalLangs: number): number => {
179
- return 300 + Math.round(totalLangs / 2) * 25;
180
- };
162
+ const calculatePieLayoutHeight = (totalLangs: number): number =>
163
+ 300 + Math.round(totalLangs / 2) * 25;
181
164
 
182
165
  /**
183
166
  * Calculates the center translation needed to keep the donut chart centred.
184
167
  *
185
- * @param totalLangs Total number of languages.
186
168
  * @returns Donut center translation.
187
169
  */
188
- const donutCenterTranslation = (totalLangs: number): number => {
189
- return -45 + Math.max(totalLangs - 5, 0) * 16;
190
- };
170
+ const donutCenterTranslation = (totalLangs: number): number =>
171
+ -45 + Math.max(totalLangs - 5, 0) * 16;
191
172
 
192
173
  /**
193
174
  * Trim top languages to lang_count while also hiding certain languages.
194
175
  *
195
- * @param topLangs Top languages.
196
- * @param langs_count Number of languages to show.
197
- * @param hide Languages to hide.
198
176
  * @returns Trimmed top languages and total size.
199
177
  */
200
178
  const trimTopLanguages = (
@@ -208,14 +186,14 @@ const trimTopLanguages = (
208
186
 
209
187
  // populate langsToHide map for quick lookup while filtering out
210
188
  if (hide) {
211
- hide.forEach((langName) => {
189
+ for (const langName of hide) {
212
190
  langsToHide[lowercaseTrim(langName)] = true;
213
- });
191
+ }
214
192
  }
215
193
 
216
194
  // filter out languages to be hidden
217
195
  langs = langs
218
- .sort((a, b) => b.size - a.size)
196
+ .toSorted((a, b) => b.size - a.size)
219
197
  .filter((lang) => !langsToHide[lowercaseTrim(lang.name)])
220
198
  .slice(0, langsCount);
221
199
 
@@ -227,20 +205,15 @@ const trimTopLanguages = (
227
205
  /**
228
206
  * Get display value corresponding to the format.
229
207
  *
230
- * @param size Bytes size.
231
- * @param percentages Percentage value.
232
- * @param format Format of the stats.
233
208
  * @returns Display value.
234
209
  */
235
- const getDisplayValue = (size: number, percentages: number, format: string): string => {
236
- return format === 'bytes' ? formatBytes(size) : `${percentages.toFixed(2)}%`;
237
- };
210
+ const getDisplayValue = (size: number, percentages: number, format: string): string =>
211
+ format === 'bytes' ? formatBytes(size) : `${percentages.toFixed(2)}%`;
238
212
 
239
213
  /**
240
214
  * Resolves a language's display color, falling back to the default, and
241
215
  * validates it is a prefixed hex color.
242
216
  *
243
- * @param lang Programming language object.
244
217
  * @returns Validated language color.
245
218
  */
246
219
  const resolveLangColor = (lang: Lang): string => {
@@ -254,15 +227,6 @@ const resolveLangColor = (lang: Lang): string => {
254
227
  /**
255
228
  * Create progress bar text item for a programming language.
256
229
  *
257
- * @param props Function properties.
258
- * @param props.width The card width
259
- * @param props.color Color of the programming language.
260
- * @param props.name Name of the programming language.
261
- * @param props.size Size of the programming language.
262
- * @param props.totalSize Total size of all languages.
263
- * @param props.statsFormat Stats format.
264
- * @param props.hideValues Whether to hide stats values.
265
- * @param props.index Index of the programming language.
266
230
  * @returns Programming language SVG node.
267
231
  */
268
232
  const createProgressTextNode = ({
@@ -283,7 +247,7 @@ const createProgressTextNode = ({
283
247
  statsFormat: string;
284
248
  hideValues?: boolean | undefined;
285
249
  index: number;
286
- }): string => {
250
+ }): MarkupElement => {
287
251
  const staggerDelay = (index + 3) * 150;
288
252
  const paddingRight = hideValues ? CARD_PADDING * 2 : 95;
289
253
  const progressTextX = width - paddingRight + 10;
@@ -292,32 +256,25 @@ const createProgressTextNode = ({
292
256
  const progress = (size / totalSize) * 100;
293
257
  const displayValue = getDisplayValue(size, progress, statsFormat);
294
258
 
295
- return `
296
- <g class="stagger" style="animation-delay: ${staggerDelay}ms">
297
- <text data-testid="lang-name" x="2" y="15" class="lang-name">${encodeHTML(name)}</text>
298
- ${hideValues ? '' : `<text x="${progressTextX}" y="34" class="lang-name">${encodeHTML(displayValue)}</text>`}
299
- ${createProgressNode({
300
- x: 0,
301
- y: 25,
302
- color,
303
- width: progressWidth,
304
- progress,
305
- delay: staggerDelay + 300,
306
- })}
307
- </g>
308
- `;
259
+ return el(
260
+ 'g',
261
+ { class: 'stagger', style: `animation-delay: ${staggerDelay}ms` },
262
+ el('text', { 'data-testid': 'lang-name', x: 2, y: 15, class: 'lang-name' }, name),
263
+ !hideValues && el('text', { x: progressTextX, y: 34, class: 'lang-name' }, displayValue),
264
+ createProgressNode({
265
+ x: 0,
266
+ y: 25,
267
+ color,
268
+ width: progressWidth,
269
+ progress,
270
+ delay: staggerDelay + 300,
271
+ }),
272
+ );
309
273
  };
310
274
 
311
275
  /**
312
276
  * Creates compact text item for a programming language.
313
277
  *
314
- * @param props Function properties.
315
- * @param props.lang Programming language object.
316
- * @param props.totalSize Total size of all languages.
317
- * @param props.hideProgress Whether to hide percentage.
318
- * @param props.hideValues Whether to hide stats values (percentages/bytes).
319
- * @param props.statsFormat Stats format
320
- * @param props.index Index of the programming language.
321
278
  * @returns Compact layout programming language SVG node.
322
279
  */
323
280
  const createCompactLangNode = ({
@@ -334,32 +291,26 @@ const createCompactLangNode = ({
334
291
  hideValues?: boolean | undefined;
335
292
  statsFormat?: string | undefined;
336
293
  index: number;
337
- }): string => {
294
+ }): MarkupElement => {
338
295
  const percentages = (lang.size / totalSize) * 100;
339
296
  const displayValue = getDisplayValue(lang.size, percentages, statsFormat);
340
297
 
341
298
  const staggerDelay = (index + 3) * 150;
342
299
  const color = resolveLangColor(lang);
343
300
 
344
- return `
345
- <g class="stagger" style="animation-delay: ${staggerDelay}ms">
346
- <circle cx="5" cy="6" r="5" fill="${color}" />
347
- <text data-testid="lang-name" x="15" y="10" class='lang-name'>
348
- ${encodeHTML(lang.name)} ${hideProgress || hideValues ? '' : encodeHTML(displayValue)}
349
- </text>
350
- </g>
351
- `;
301
+ const label = hideProgress || hideValues ? lang.name : `${lang.name} ${displayValue}`;
302
+
303
+ return el(
304
+ 'g',
305
+ { class: 'stagger', style: `animation-delay: ${staggerDelay}ms` },
306
+ el('circle', { cx: 5, cy: 6, r: 5, fill: color }),
307
+ el('text', { 'data-testid': 'lang-name', x: 15, y: 10, class: 'lang-name' }, label),
308
+ );
352
309
  };
353
310
 
354
311
  /**
355
312
  * Create compact languages text items for all programming languages.
356
313
  *
357
- * @param props Function properties.
358
- * @param props.langs Array of programming languages.
359
- * @param props.totalSize Total size of all languages.
360
- * @param props.hideProgress Whether to hide percentage.
361
- * @param props.hideValues Whether to hide stats values.
362
- * @param props.statsFormat Stats format
363
314
  * @returns Programming languages SVG node.
364
315
  */
365
316
  const createLanguageTextNode = ({
@@ -374,7 +325,7 @@ const createLanguageTextNode = ({
374
325
  hideProgress?: boolean | undefined;
375
326
  hideValues?: boolean | undefined;
376
327
  statsFormat?: string | undefined;
377
- }): string => {
328
+ }): Array<Child> => {
378
329
  const longestLang = getLongestLang(langs);
379
330
  const chunked = chunkArray(langs, langs.length / 2);
380
331
  const layouts = chunked.map((array) => {
@@ -388,30 +339,18 @@ const createLanguageTextNode = ({
388
339
  index,
389
340
  }),
390
341
  );
391
- return flexLayout({
392
- items,
393
- gap: 25,
394
- direction: 'column',
395
- }).join('');
342
+ return flexLayout({ items, gap: 25, direction: 'column' });
396
343
  });
397
344
 
398
345
  const percent = ((longestLang.size / totalSize) * 100).toFixed(2);
399
346
  const minGap = 150;
400
347
  const maxGap = 20 + measureText(`${longestLang.name} ${percent}%`, 11);
401
- return flexLayout({
402
- items: layouts,
403
- gap: maxGap < minGap ? minGap : maxGap,
404
- }).join('');
348
+ return flexLayout({ items: layouts, gap: maxGap < minGap ? minGap : maxGap });
405
349
  };
406
350
 
407
351
  /**
408
352
  * Create donut languages text items for all programming languages.
409
353
  *
410
- * @param props Function properties.
411
- * @param props.langs Array of programming languages.
412
- * @param props.totalSize Total size of all languages.
413
- * @param props.hideValues Whether to hide stats values.
414
- * @param props.statsFormat Stats format
415
354
  * @returns Donut layout programming language SVG node.
416
355
  */
417
356
  const createDonutLanguagesNode = ({
@@ -424,31 +363,25 @@ const createDonutLanguagesNode = ({
424
363
  totalSize: number;
425
364
  hideValues?: boolean | undefined;
426
365
  statsFormat?: string | undefined;
427
- }): string => {
428
- return flexLayout({
429
- items: langs.map((lang, index) => {
430
- return createCompactLangNode({
366
+ }): Array<Child> =>
367
+ flexLayout({
368
+ items: langs.map((lang, index) =>
369
+ createCompactLangNode({
431
370
  lang,
432
371
  totalSize,
433
372
  hideProgress: false,
434
373
  hideValues,
435
374
  statsFormat,
436
375
  index,
437
- });
438
- }),
376
+ }),
377
+ ),
439
378
  gap: 32,
440
379
  direction: 'column',
441
- }).join('');
442
- };
380
+ });
443
381
 
444
382
  /**
445
383
  * Renders the default language card layout.
446
384
  *
447
- * @param langs Array of programming languages.
448
- * @param width Card width.
449
- * @param totalLanguageSize Total size of all languages.
450
- * @param statsFormat Stats format.
451
- * @param hideValues Whether to hide stats values.
452
385
  * @returns Normal layout card SVG object.
453
386
  */
454
387
  const renderNormalLayout = (
@@ -457,10 +390,10 @@ const renderNormalLayout = (
457
390
  totalLanguageSize: number,
458
391
  statsFormat: string,
459
392
  hideValues?: boolean,
460
- ): string => {
461
- return flexLayout({
462
- items: langs.map((lang, index) => {
463
- return createProgressTextNode({
393
+ ): Array<Child> =>
394
+ flexLayout({
395
+ items: langs.map((lang, index) =>
396
+ createProgressTextNode({
464
397
  width,
465
398
  name: lang.name,
466
399
  color: lang.color || DEFAULT_LANG_COLOR,
@@ -469,22 +402,15 @@ const renderNormalLayout = (
469
402
  statsFormat,
470
403
  hideValues,
471
404
  index,
472
- });
473
- }),
405
+ }),
406
+ ),
474
407
  gap: 40,
475
408
  direction: 'column',
476
- }).join('');
477
- };
409
+ });
478
410
 
479
411
  /**
480
412
  * Renders the compact language card layout.
481
413
  *
482
- * @param langs Array of programming languages.
483
- * @param width Card width.
484
- * @param totalLanguageSize Total size of all languages.
485
- * @param hideProgress Whether to hide progress bar.
486
- * @param statsFormat Stats format.
487
- * @param hideValues Whether to hide stats values.
488
414
  * @returns Compact layout card SVG object.
489
415
  */
490
416
  const renderCompactLayout = (
@@ -494,66 +420,56 @@ const renderCompactLayout = (
494
420
  hideProgress?: boolean,
495
421
  statsFormat = 'percentages',
496
422
  hideValues?: boolean,
497
- ): string => {
423
+ ): Array<Child> => {
498
424
  const paddingRight = 50;
499
425
  const offsetWidth = width - paddingRight;
500
426
  // progressOffset holds the previous language's width and used to offset the next language
501
427
  // so that we can stack them one after another, like this: [--][----][---]
502
428
  let progressOffset = 0;
503
- const compactProgressBar = langs
504
- .map((lang) => {
505
- const langColor = resolveLangColor(lang);
506
-
507
- const percentage = parseFloat(((lang.size / totalLanguageSize) * offsetWidth).toFixed(2));
508
-
509
- const progress = percentage < 10 ? percentage + 10 : percentage;
510
-
511
- const output = `
512
- <rect
513
- mask="url(#rect-mask)"
514
- data-testid="lang-progress"
515
- x="${progressOffset}"
516
- y="0"
517
- width="${progress}"
518
- height="8"
519
- fill="${langColor}"
520
- />
521
- `;
522
- progressOffset += percentage;
523
- return output;
524
- })
525
- .join('');
526
-
527
- return `
528
- ${
529
- hideProgress
530
- ? ''
531
- : `
532
- <mask id="rect-mask">
533
- <rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
534
- </mask>
535
- ${compactProgressBar}
536
- `
537
- }
538
- <g transform="translate(0, ${hideProgress ? '0' : '25'})">
539
- ${createLanguageTextNode({
429
+ const compactProgressBar = langs.map((lang) => {
430
+ const percentage = Number.parseFloat(
431
+ ((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
432
+ );
433
+ const x = progressOffset;
434
+ progressOffset += percentage;
435
+
436
+ return el('rect', {
437
+ mask: 'url(#rect-mask)',
438
+ 'data-testid': 'lang-progress',
439
+ x,
440
+ y: 0,
441
+ width: percentage < 10 ? percentage + 10 : percentage,
442
+ height: 8,
443
+ fill: resolveLangColor(lang),
444
+ });
445
+ });
446
+
447
+ return [
448
+ !hideProgress && [
449
+ el(
450
+ 'mask',
451
+ { id: 'rect-mask' },
452
+ el('rect', { x: 0, y: 0, width: offsetWidth, height: 8, fill: 'white', rx: 5 }),
453
+ ),
454
+ compactProgressBar,
455
+ ],
456
+ el(
457
+ 'g',
458
+ { transform: `translate(0, ${hideProgress ? 0 : 25})` },
459
+ createLanguageTextNode({
540
460
  langs,
541
461
  totalSize: totalLanguageSize,
542
462
  hideProgress,
543
463
  statsFormat,
544
464
  hideValues,
545
- })}
546
- </g>
547
- `;
465
+ }),
466
+ ),
467
+ ];
548
468
  };
549
469
 
550
470
  /**
551
471
  * Renders donut vertical layout to display user's most frequently used programming languages.
552
472
  *
553
- * @param langs Array of programming languages.
554
- * @param totalLanguageSize Total size of all languages.
555
- * @param statsFormat Stats format.
556
- * @param hideValues Whether to hide stats values.
557
473
  * @returns Compact layout card SVG object.
558
474
  */
559
475
  const renderDonutVerticalLayout = (
@@ -561,7 +477,7 @@ const renderDonutVerticalLayout = (
561
477
  totalLanguageSize: number,
562
478
  statsFormat: string,
563
479
  hideValues?: boolean,
564
- ): string => {
480
+ ): MarkupElement => {
565
481
  // Donut vertical chart radius and total length
566
482
  const radius = 80;
567
483
  const totalCircleLength = getCircleLength(radius);
@@ -583,22 +499,24 @@ const renderDonutVerticalLayout = (
583
499
  const circleLength = totalCircleLength * (percentage / 100);
584
500
  const delay = startDelayCoefficient * 100;
585
501
 
586
- circles.push(`
587
- <g class="stagger" style="animation-delay: ${delay}ms">
588
- <circle
589
- cx="150"
590
- cy="100"
591
- r="${radius}"
592
- fill="transparent"
593
- stroke="${langColor}"
594
- stroke-width="25"
595
- stroke-dasharray="${totalCircleLength}"
596
- stroke-dashoffset="${indent}"
597
- size="${percentage}"
598
- data-testid="lang-donut"
599
- />
600
- </g>
601
- `);
502
+ circles.push(
503
+ el(
504
+ 'g',
505
+ { class: 'stagger', style: `animation-delay: ${delay}ms` },
506
+ el('circle', {
507
+ cx: 150,
508
+ cy: 100,
509
+ r: radius,
510
+ fill: 'transparent',
511
+ stroke: langColor,
512
+ 'stroke-width': 25,
513
+ 'stroke-dasharray': totalCircleLength,
514
+ 'stroke-dashoffset': indent,
515
+ size: percentage,
516
+ 'data-testid': 'lang-donut',
517
+ }),
518
+ ),
519
+ );
602
520
 
603
521
  // Update the indent for the next part
604
522
  indent += circleLength;
@@ -606,35 +524,31 @@ const renderDonutVerticalLayout = (
606
524
  startDelayCoefficient += 1;
607
525
  }
608
526
 
609
- return `
610
- <svg data-testid="lang-items">
611
- <g transform="translate(0, 0)">
612
- <svg data-testid="donut">
613
- ${circles.join('')}
614
- </svg>
615
- </g>
616
- <g transform="translate(0, 220)">
617
- <svg data-testid="lang-names" x="${CARD_PADDING}">
618
- ${createLanguageTextNode({
619
- langs,
620
- totalSize: totalLanguageSize,
621
- hideProgress: false,
622
- statsFormat,
623
- hideValues,
624
- })}
625
- </svg>
626
- </g>
627
- </svg>
628
- `;
527
+ return el(
528
+ 'svg',
529
+ { 'data-testid': 'lang-items' },
530
+ el('svg', { 'data-testid': 'donut' }, circles),
531
+ el(
532
+ 'g',
533
+ { transform: 'translate(0, 220)' },
534
+ el(
535
+ 'svg',
536
+ { 'data-testid': 'lang-names', x: CARD_PADDING },
537
+ createLanguageTextNode({
538
+ langs,
539
+ totalSize: totalLanguageSize,
540
+ hideProgress: false,
541
+ statsFormat,
542
+ hideValues,
543
+ }),
544
+ ),
545
+ ),
546
+ );
629
547
  };
630
548
 
631
549
  /**
632
550
  * Renders pie layout to display user's most frequently used programming languages.
633
551
  *
634
- * @param langs Array of programming languages.
635
- * @param totalLanguageSize Total size of all languages.
636
- * @param statsFormat Stats format.
637
- * @param hideValues Whether to hide stats values.
638
552
  * @returns Compact layout card SVG object.
639
553
  */
640
554
  const renderPieLayout = (
@@ -642,7 +556,7 @@ const renderPieLayout = (
642
556
  totalLanguageSize: number,
643
557
  statsFormat: string,
644
558
  hideValues?: boolean,
645
- ): string => {
559
+ ): MarkupElement => {
646
560
  // Pie chart radius and center coordinates
647
561
  const radius = 90;
648
562
  const centerX = 150;
@@ -662,17 +576,17 @@ const renderPieLayout = (
662
576
  const langColor = resolveLangColor(lang);
663
577
 
664
578
  if (langs.length === 1) {
665
- paths.push(`
666
- <circle
667
- cx="${centerX}"
668
- cy="${centerY}"
669
- r="${radius}"
670
- stroke="none"
671
- fill="${langColor}"
672
- data-testid="lang-pie"
673
- size="100"
674
- />
675
- `);
579
+ paths.push(
580
+ el('circle', {
581
+ cx: centerX,
582
+ cy: centerY,
583
+ r: radius,
584
+ stroke: 'none',
585
+ fill: langColor,
586
+ 'data-testid': 'lang-pie',
587
+ size: 100,
588
+ }),
589
+ );
676
590
  break;
677
591
  }
678
592
 
@@ -695,16 +609,18 @@ const renderPieLayout = (
695
609
  const delay = startDelayCoefficient * 100;
696
610
 
697
611
  // SVG arc markup
698
- paths.push(`
699
- <g class="stagger" style="animation-delay: ${delay}ms">
700
- <path
701
- data-testid="lang-pie"
702
- size="${percentage}"
703
- d="M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z"
704
- fill="${langColor}"
705
- />
706
- </g>
707
- `);
612
+ paths.push(
613
+ el(
614
+ 'g',
615
+ { class: 'stagger', style: `animation-delay: ${delay}ms` },
616
+ el('path', {
617
+ 'data-testid': 'lang-pie',
618
+ size: percentage,
619
+ d: `M ${centerX} ${centerY} L ${startPoint.x} ${startPoint.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endPoint.x} ${endPoint.y} Z`,
620
+ fill: langColor,
621
+ }),
622
+ ),
623
+ );
708
624
 
709
625
  // Update the start angle for the next part
710
626
  startAngle = endAngle;
@@ -712,35 +628,31 @@ const renderPieLayout = (
712
628
  startDelayCoefficient += 1;
713
629
  }
714
630
 
715
- return `
716
- <svg data-testid="lang-items">
717
- <g transform="translate(0, 0)">
718
- <svg data-testid="pie">
719
- ${paths.join('')}
720
- </svg>
721
- </g>
722
- <g transform="translate(0, 220)">
723
- <svg data-testid="lang-names" x="${CARD_PADDING}">
724
- ${createLanguageTextNode({
725
- langs,
726
- totalSize: totalLanguageSize,
727
- hideProgress: false,
728
- statsFormat,
729
- hideValues,
730
- })}
731
- </svg>
732
- </g>
733
- </svg>
734
- `;
631
+ return el(
632
+ 'svg',
633
+ { 'data-testid': 'lang-items' },
634
+ el('svg', { 'data-testid': 'pie' }, paths),
635
+ el(
636
+ 'g',
637
+ { transform: 'translate(0, 220)' },
638
+ el(
639
+ 'svg',
640
+ { 'data-testid': 'lang-names', x: CARD_PADDING },
641
+ createLanguageTextNode({
642
+ langs,
643
+ totalSize: totalLanguageSize,
644
+ hideProgress: false,
645
+ statsFormat,
646
+ hideValues,
647
+ }),
648
+ ),
649
+ ),
650
+ );
735
651
  };
736
652
 
737
653
  /**
738
654
  * Creates the SVG paths for the language donut chart.
739
655
  *
740
- * @param cx Donut center x-position.
741
- * @param cy Donut center y-position.
742
- * @param radius Donut arc Radius.
743
- * @param percentages Array with donut section percentages.
744
656
  * @returns Array of svg path elements
745
657
  */
746
658
  const createDonutPaths = (
@@ -754,7 +666,7 @@ const createDonutPaths = (
754
666
 
755
667
  const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0);
756
668
  for (const rawPercent of percentages) {
757
- const percent = parseFloat(((rawPercent / totalPercent) * 100).toFixed(2));
669
+ const percent = Number.parseFloat(((rawPercent / totalPercent) * 100).toFixed(2));
758
670
 
759
671
  const endAngle = 3.6 * percent + startAngle;
760
672
  const startPoint = polarToCartesian(cx, cy, radius, endAngle - 90); // rotate donut 90 degrees counter-clockwise.
@@ -774,11 +686,6 @@ const createDonutPaths = (
774
686
  /**
775
687
  * Renders the donut language card layout.
776
688
  *
777
- * @param langs Array of programming languages.
778
- * @param width Card width.
779
- * @param totalLanguageSize Total size of all languages.
780
- * @param statsFormat Stats format.
781
- * @param hideValues Whether to hide stats values.
782
689
  * @returns Donut layout card SVG object.
783
690
  */
784
691
  const renderDonutLayout = (
@@ -787,9 +694,9 @@ const renderDonutLayout = (
787
694
  totalLanguageSize: number,
788
695
  statsFormat: string,
789
696
  hideValues?: boolean,
790
- ): string => {
697
+ ): Array<Child> => {
791
698
  if (!Number.isFinite(width)) {
792
- throw new Error(`Invalid width: "${width}"`);
699
+ throw new TypeError(`Invalid width: "${width}"`);
793
700
  }
794
701
 
795
702
  const centerX = width / 3;
@@ -797,59 +704,55 @@ const renderDonutLayout = (
797
704
  const radius = centerX - 60;
798
705
  const strokeWidth = 12;
799
706
 
800
- const colors = langs.map(resolveLangColor);
707
+ const colors = langs.map((lang) => resolveLangColor(lang));
801
708
  const langsPercents = langs.map((lang) =>
802
- parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
709
+ Number.parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
803
710
  );
804
711
 
805
712
  const langPaths = createDonutPaths(centerX, centerY, radius, langsPercents);
806
713
 
807
- const donutPaths =
714
+ const donutPaths: Array<Child> =
808
715
  langs.length === 1
809
- ? `<circle cx="${centerX}" cy="${centerY}" r="${radius}" stroke="${colors[0] ?? DEFAULT_LANG_COLOR}" fill="none" stroke-width="${strokeWidth}" data-testid="lang-donut" size="100"/>`
810
- : langPaths
811
- .map((section, index) => {
812
- const staggerDelay = (index + 3) * 100;
813
- const delay = staggerDelay + 300;
814
-
815
- const output = `
816
- <g class="stagger" style="animation-delay: ${delay}ms">
817
- <path
818
- data-testid="lang-donut"
819
- size="${section.percent}"
820
- d="${section.d}"
821
- stroke="${colors[index] ?? DEFAULT_LANG_COLOR}"
822
- fill="none"
823
- stroke-width="${strokeWidth}">
824
- </path>
825
- </g>
826
- `;
827
-
828
- return output;
829
- })
830
- .join('');
831
-
832
- const donut = `<svg width="${width}" height="${width}">${donutPaths}</svg>`;
833
-
834
- return `
835
- <g transform="translate(0, 0)">
836
- <g transform="translate(0, 0)">
837
- ${createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, hideValues, statsFormat })}
838
- </g>
839
-
840
- <g transform="translate(125, ${donutCenterTranslation(langs.length)})">
841
- ${donut}
842
- </g>
843
- </g>
844
- `;
716
+ ? [
717
+ el('circle', {
718
+ cx: centerX,
719
+ cy: centerY,
720
+ r: radius,
721
+ stroke: colors[0] ?? DEFAULT_LANG_COLOR,
722
+ fill: 'none',
723
+ 'stroke-width': strokeWidth,
724
+ 'data-testid': 'lang-donut',
725
+ size: 100,
726
+ }),
727
+ ]
728
+ : langPaths.map((section, index) =>
729
+ el(
730
+ 'g',
731
+ { class: 'stagger', style: `animation-delay: ${(index + 3) * 100 + 300}ms` },
732
+ el('path', {
733
+ 'data-testid': 'lang-donut',
734
+ size: section.percent,
735
+ d: section.d,
736
+ stroke: colors[index] ?? DEFAULT_LANG_COLOR,
737
+ fill: 'none',
738
+ 'stroke-width': strokeWidth,
739
+ }),
740
+ ),
741
+ );
742
+
743
+ return [
744
+ createDonutLanguagesNode({ langs, totalSize: totalLanguageSize, hideValues, statsFormat }),
745
+ el(
746
+ 'g',
747
+ { transform: `translate(125, ${donutCenterTranslation(langs.length)})` },
748
+ el('svg', { width, height: width }, donutPaths),
749
+ ),
750
+ ];
845
751
  };
846
752
 
847
753
  /**
848
754
  * Creates the no languages data SVG node.
849
755
  *
850
- * @param props Object with function properties.
851
- * @param props.text No languages data translated text.
852
- * @param props.layout Card layout.
853
756
  * @returns No languages data SVG node string.
854
757
  */
855
758
  const noLanguagesDataNode = ({
@@ -858,20 +761,20 @@ const noLanguagesDataNode = ({
858
761
  }: {
859
762
  text: string;
860
763
  layout: TopLangLayout | undefined;
861
- }): string => {
862
- return `
863
- <text x="${
864
- layout === 'pie' || layout === 'donut-vertical' ? CARD_PADDING : 0
865
- }" y="11" class="stat bold">${encodeHTML(text)}</text>
866
- `;
867
- };
764
+ }): MarkupElement =>
765
+ el(
766
+ 'text',
767
+ {
768
+ x: layout === 'pie' || layout === 'donut-vertical' ? CARD_PADDING : 0,
769
+ y: 11,
770
+ class: 'stat bold',
771
+ },
772
+ text,
773
+ );
868
774
 
869
775
  /**
870
776
  * Get default languages count for provided card layout.
871
777
  *
872
- * @param props Function properties.
873
- * @param props.layout Input layout string.
874
- * @param props.hide_progress Input hide_progress parameter value.
875
778
  * @returns Default languages count for input layout.
876
779
  */
877
780
  const getDefaultLanguagesCountByLayout = ({
@@ -883,28 +786,25 @@ const getDefaultLanguagesCountByLayout = ({
883
786
  }): number => {
884
787
  if (layout === 'compact' || hide_progress === true) {
885
788
  return COMPACT_LAYOUT_DEFAULT_LANGS_COUNT;
886
- } else if (layout === 'donut') {
789
+ }
790
+ if (layout === 'donut') {
887
791
  return DONUT_LAYOUT_DEFAULT_LANGS_COUNT;
888
- } else if (layout === 'donut-vertical') {
792
+ }
793
+ if (layout === 'donut-vertical') {
889
794
  return DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT;
890
- } else if (layout === 'pie') {
795
+ }
796
+ if (layout === 'pie') {
891
797
  return PIE_LAYOUT_DEFAULT_LANGS_COUNT;
892
- } else {
893
- return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT;
894
798
  }
799
+ return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT;
895
800
  };
896
801
 
897
802
  /**
898
803
  * Renders card that display user's most frequently used programming languages.
899
804
  *
900
- * @param topLangs User's most frequently used programming languages.
901
- * @param options Card options.
902
805
  * @returns Language card SVG object.
903
806
  */
904
- const renderTopLanguages = (
905
- topLangs: TopLangData,
906
- options: CardOptions<TopLangOptions> = {},
907
- ): string => {
807
+ const renderCard = (topLangs: TopLangData, options: CardOptions<TopLangOptions> = {}): string => {
908
808
  const {
909
809
  hide_title = false,
910
810
  hide_border = false,
@@ -929,7 +829,7 @@ const renderTopLanguages = (
929
829
  const { langs, totalLanguageSize } = trimTopLanguages(topLangs, langs_count, hide);
930
830
 
931
831
  let width = card_width
932
- ? isNaN(card_width)
832
+ ? Number.isNaN(card_width)
933
833
  ? DEFAULT_CARD_WIDTH
934
834
  : card_width < MIN_CARD_WIDTH
935
835
  ? MIN_CARD_WIDTH
@@ -939,7 +839,7 @@ const renderTopLanguages = (
939
839
 
940
840
  const { lightColors, darkColors } = getLightDarkColors(options);
941
841
 
942
- let finalLayout: string;
842
+ let finalLayout: Child;
943
843
  if (langs.length === 0) {
944
844
  height = COMPACT_LAYOUT_BASE_HEIGHT;
945
845
  finalLayout = noLanguagesDataNode({
@@ -965,7 +865,9 @@ const renderTopLanguages = (
965
865
  );
966
866
  } else if (layout === 'donut') {
967
867
  height = calculateDonutLayoutHeight(langs.length);
968
- width = width + 50; // padding
868
+ // A donut defaults a step wider so the chart has its room; a width asked for
869
+ // explicitly keeps the extra it has always been given instead of being snapped.
870
+ width = width === DEFAULT_CARD_WIDTH ? CARD_WIDTH.standard : width + DONUT_EXTRA_WIDTH;
969
871
  finalLayout = renderDonutLayout(langs, width, totalLanguageSize, stats_format, hide_values);
970
872
  } else {
971
873
  finalLayout = renderNormalLayout(langs, width, totalLanguageSize, stats_format, hide_values);
@@ -974,6 +876,7 @@ const renderTopLanguages = (
974
876
  const card = new Card({
975
877
  customTitle: custom_title,
976
878
  defaultTitle: i18n.t('langcard.title'),
879
+ titlePrefixIcon: CARD_ICON.topLanguages,
977
880
  width,
978
881
  height,
979
882
  border_radius,
@@ -987,52 +890,36 @@ const renderTopLanguages = (
987
890
  card.setHideBorder(hide_border);
988
891
  card.setHideTitle(hide_title);
989
892
  card.setCSS({
990
- light: ({ textColor, progBarBgColor }) => `
991
- @keyframes slideInAnimation {
992
- from {
993
- width: 0;
994
- }
995
- to {
996
- width: calc(100%-100px);
997
- }
998
- }
999
- @keyframes growWidthAnimation {
1000
- from {
1001
- width: 0;
1002
- }
1003
- to {
1004
- width: 100%;
1005
- }
1006
- }
1007
- .stat {
1008
- font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
1009
- }
1010
- @supports(-moz-appearance: auto) {
1011
- /* Selector detects Firefox */
1012
- .stat { font-size:12px; }
1013
- }
1014
- .bold { font-weight: 700 }
1015
- .lang-name {
1016
- font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
1017
- fill: ${textColor};
1018
- }
1019
- .stagger {
1020
- opacity: 0;
1021
- animation: fadeInAnimation 0.3s ease-in-out forwards;
1022
- }
1023
- #rect-mask rect{
1024
- animation: slideInAnimation 1s ease-in-out forwards;
1025
- }
1026
- .lang-progress{
1027
- animation: growWidthAnimation 0.6s ease-in-out forwards;
1028
- }
1029
- .progress-background { fill: ${progBarBgColor}; }
1030
- `,
1031
- dark: ({ textColor, progBarBgColor }) => `
1032
- .stat { fill: ${textColor}; }
1033
- .lang-name { fill: ${textColor}; }
1034
- .progress-background { fill: ${progBarBgColor}; }
1035
- `,
893
+ light: ({ textColor, progBarBgColor }) => [
894
+ atRule(
895
+ '@keyframes slideInAnimation',
896
+ rule('from', { width: 0 }),
897
+ // Invalid on purpose — `calc` needs spaces around its `-`. Browsers drop the
898
+ // declaration and animate to the rect's own width, which is the reveal the mask wants.
899
+ rule('to', { width: 'calc(100%-100px)' }),
900
+ ),
901
+ atRule(
902
+ '@keyframes growWidthAnimation',
903
+ rule('from', { width: 0 }),
904
+ rule('to', { width: '100%' }),
905
+ ),
906
+ rule('.stat', {
907
+ font: font('regular', 'body'),
908
+ fill: textColor,
909
+ }),
910
+ firefoxFontSize(['.stat'], 'small'),
911
+ rule('.bold', { 'font-weight': FONT_WEIGHT.semibold }),
912
+ rule('.lang-name', { font: font('regular', 'micro'), fill: textColor }),
913
+ rule('.stagger', { opacity: 0, animation: 'fadeInAnimation 0.3s ease-in-out forwards' }),
914
+ rule('#rect-mask rect', { animation: 'slideInAnimation 1s ease-in-out forwards' }),
915
+ rule('.lang-progress', { animation: 'growWidthAnimation 0.6s ease-in-out forwards' }),
916
+ rule('.progress-background', { fill: progBarBgColor }),
917
+ ],
918
+ dark: ({ textColor, progBarBgColor }) => [
919
+ rule('.stat', { fill: textColor }),
920
+ rule('.lang-name', { fill: textColor }),
921
+ rule('.progress-background', { fill: progBarBgColor }),
922
+ ],
1036
923
  });
1037
924
 
1038
925
  // `role="img"` hides the inner text from assistive tech, so everything the card
@@ -1048,13 +935,21 @@ const renderTopLanguages = (
1048
935
  return card.render(finalLayout);
1049
936
  }
1050
937
 
1051
- return card.render(`
1052
- <svg data-testid="lang-items" x="${CARD_PADDING}">
1053
- ${finalLayout}
1054
- </svg>
1055
- `);
938
+ return card.render(el('svg', { 'data-testid': 'lang-items', x: CARD_PADDING }, finalLayout));
1056
939
  };
1057
940
 
941
+ /**
942
+ * The card, and the values each of its options accepts, keyed by the option's own name.
943
+ * They ride on the renderer so a list cannot be found without what draws it;
944
+ * the api handler forwards them onto its own export, which is what a UI reads.
945
+ */
946
+ const renderTopLanguages = Object.assign(renderCard, {
947
+ OPTIONS: {
948
+ layout: TOP_LANG_LAYOUTS,
949
+ stats_format: TOP_LANG_STATS_FORMATS,
950
+ },
951
+ });
952
+
1058
953
  export {
1059
954
  getLongestLang,
1060
955
  degreesToRadians,
@@ -1069,9 +964,7 @@ export {
1069
964
  calculatePieLayoutHeight,
1070
965
  donutCenterTranslation,
1071
966
  trimTopLanguages,
1072
- renderTopLanguages,
1073
967
  MIN_CARD_WIDTH,
1074
968
  getDefaultLanguagesCountByLayout,
1075
- TOP_LANG_LAYOUTS,
1076
- TOP_LANG_STATS_FORMATS,
969
+ renderTopLanguages,
1077
970
  };