@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
@@ -0,0 +1,118 @@
1
+ import type { Sink } from './serialize.ts';
2
+ import { INDENT, indentAt } from './serialize.ts';
3
+
4
+ /** Every CSS property a card declares, closed so a typo is a compile error, not a dropped rule. */
5
+ type CssProperty =
6
+ | '-webkit-box-orient'
7
+ | '-webkit-line-clamp'
8
+ | 'animation'
9
+ | 'animation-delay'
10
+ | 'animation-duration'
11
+ | 'color'
12
+ | 'display'
13
+ | 'fill'
14
+ | 'font'
15
+ | 'font-size'
16
+ | 'font-variant-numeric'
17
+ | 'font-weight'
18
+ | 'line-clamp'
19
+ | 'line-height'
20
+ | 'margin'
21
+ | 'opacity'
22
+ | 'overflow'
23
+ | 'overflow-wrap'
24
+ | 'padding-bottom'
25
+ | 'stroke'
26
+ | 'stroke-dasharray'
27
+ | 'stroke-dashoffset'
28
+ | 'stroke-linecap'
29
+ | 'stroke-width'
30
+ | 'text-overflow'
31
+ | 'transform'
32
+ | 'transform-origin'
33
+ | 'width'
34
+ | 'word-break';
35
+
36
+ /** A declaration whose value is `undefined` is left out, so a caller can inline a condition. */
37
+ type Declarations = Partial<Record<CssProperty, string | number | undefined>>;
38
+
39
+ type CssNode =
40
+ | { readonly kind: 'rule'; readonly selector: string; readonly declarations: Declarations }
41
+ | { readonly kind: 'at'; readonly prelude: string; readonly children: Array<CssChild> }
42
+ | { readonly kind: 'comment'; readonly text: string };
43
+
44
+ /** Anything accepted where CSS is expected; falsy entries drop out, arrays flatten. */
45
+ type CssChild = CssNode | false | null | undefined | Array<CssChild>;
46
+
47
+ /**
48
+ * Selectors stay free-form: they compose (`.badge rect`), and a keyframe stop is one too (`from`).
49
+ */
50
+ const rule = (selector: string, declarations: Declarations): CssNode => ({
51
+ kind: 'rule',
52
+ selector,
53
+ declarations,
54
+ });
55
+
56
+ const atRule = (prelude: string, ...children: Array<CssChild>): CssNode => ({
57
+ kind: 'at',
58
+ prelude,
59
+ children,
60
+ });
61
+
62
+ const cssComment = (text: string): CssNode => ({ kind: 'comment', text });
63
+
64
+ /** Drops the rules that carry nothing and flattens the arrays. */
65
+ const flatten = (children: Array<CssChild>, into: Array<CssNode> = []): Array<CssNode> => {
66
+ for (const child of children) {
67
+ if (!child) {
68
+ continue;
69
+ }
70
+ if (Array.isArray(child)) {
71
+ flatten(child, into);
72
+ } else {
73
+ into.push(child);
74
+ }
75
+ }
76
+ return into;
77
+ };
78
+
79
+ /**
80
+ * Serializes a stylesheet, dropping every rule that would have come out empty.
81
+ *
82
+ * A block is written optimistically and rewound if its body turns out empty,
83
+ * so the tree is walked once however deeply the at-rules nest.
84
+ */
85
+ const writeCss = (children: Array<CssChild>, depth: number, sink: Sink): void => {
86
+ const indent = indentAt(depth);
87
+
88
+ for (const node of flatten(children)) {
89
+ if (node.kind === 'comment') {
90
+ sink.out += `${indent}/* ${node.text} */\n`;
91
+ continue;
92
+ }
93
+
94
+ const mark = sink.out.length;
95
+
96
+ if (node.kind === 'at') {
97
+ sink.out += `${indent}${node.prelude} {\n`;
98
+ const bodyStart = sink.out.length;
99
+ writeCss(node.children, depth + 1, sink);
100
+ sink.out =
101
+ sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}}\n`;
102
+ continue;
103
+ }
104
+
105
+ sink.out += `${indent}${node.selector} {\n`;
106
+ const bodyStart = sink.out.length;
107
+ for (const property of Object.keys(node.declarations) as Array<CssProperty>) {
108
+ const value = node.declarations[property];
109
+ if (value !== undefined) {
110
+ sink.out += `${indent}${INDENT}${property}: ${String(value)};\n`;
111
+ }
112
+ }
113
+ sink.out = sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}}\n`;
114
+ }
115
+ };
116
+
117
+ export { atRule, cssComment, rule, writeCss };
118
+ export type { CssChild, Declarations };
@@ -0,0 +1,246 @@
1
+ import type { CssChild } from './css.ts';
2
+ import { writeCss } from './css.ts';
3
+ import { encodeHTML } from './html.ts';
4
+ import type { Sink } from './serialize.ts';
5
+ import { indentAt } from './serialize.ts';
6
+
7
+ /**
8
+ * Every element a card draws, closed so `el('circel', …)` is a compile error.
9
+ * `style` is absent on purpose: it comes from `style()`, which takes rules rather than children.
10
+ */
11
+ type Tag =
12
+ | 'a'
13
+ | 'circle'
14
+ | 'defs'
15
+ | 'desc'
16
+ | 'div'
17
+ | 'foreignObject'
18
+ | 'g'
19
+ | 'linearGradient'
20
+ | 'mask'
21
+ | 'path'
22
+ | 'rect'
23
+ | 'stop'
24
+ | 'svg'
25
+ | 'text'
26
+ | 'title'
27
+ | 'tspan';
28
+
29
+ /**
30
+ * Every attribute a card writes, closed like `Tag`:
31
+ * a misspelled `stroke-width` would otherwise be valid TypeScript and invisible in the output.
32
+ */
33
+ type Attribute =
34
+ | 'alignment-baseline'
35
+ | 'aria-hidden'
36
+ | 'aria-labelledby'
37
+ | 'class'
38
+ | 'cx'
39
+ | 'cy'
40
+ | 'd'
41
+ | 'data-testid'
42
+ | 'data-view-component'
43
+ | 'dominant-baseline'
44
+ | 'dy'
45
+ | 'fill'
46
+ | 'fill-rule'
47
+ | 'gradientTransform'
48
+ | 'gradientUnits'
49
+ | 'height'
50
+ | 'href'
51
+ | 'id'
52
+ | 'mask'
53
+ | 'offset'
54
+ | 'r'
55
+ | 'role'
56
+ | 'rx'
57
+ | 'ry'
58
+ | 'size'
59
+ | 'stop-color'
60
+ | 'stroke'
61
+ | 'stroke-dasharray'
62
+ | 'stroke-dashoffset'
63
+ | 'stroke-linecap'
64
+ | 'stroke-opacity'
65
+ | 'stroke-width'
66
+ | 'style'
67
+ | 'text-anchor'
68
+ | 'transform'
69
+ | 'version'
70
+ | 'viewBox'
71
+ | 'width'
72
+ | 'x'
73
+ | 'xmlns'
74
+ | 'y';
75
+
76
+ /** An attribute whose value is `undefined` is not written, so a caller can inline a condition. */
77
+ type Attributes = Partial<Record<Attribute, string | number | undefined>>;
78
+
79
+ /** Elements whose character data is part of the rendering, so they stay on one line. */
80
+ const INLINE_ELEMENTS: ReadonlySet<Tag> = new Set(['text', 'tspan', 'title', 'desc', 'div']);
81
+
82
+ interface MarkupElement {
83
+ readonly tag: Tag;
84
+ readonly attributes: Attributes;
85
+ readonly children: Array<Child>;
86
+ }
87
+
88
+ interface StyleElement {
89
+ readonly tag: 'style';
90
+ readonly rules: Array<CssChild>;
91
+ }
92
+
93
+ /** Anything accepted as content; falsy entries drop out, arrays flatten, strings are escaped. */
94
+ type Child =
95
+ | MarkupElement
96
+ | StyleElement
97
+ | string
98
+ | number
99
+ | false
100
+ | null
101
+ | undefined
102
+ | Array<Child>;
103
+
104
+ /** What is left of a child list once the empty entries and the nesting are gone. */
105
+ type FlatChild = MarkupElement | StyleElement | string | number;
106
+
107
+ /**
108
+ * Builds one element. Attribute values and text children are encoded on the way out.
109
+ */
110
+ const el = (tag: Tag, attributes: Attributes = {}, ...children: Array<Child>): MarkupElement => ({
111
+ tag,
112
+ attributes,
113
+ children,
114
+ });
115
+
116
+ /**
117
+ * Builds the `<style>` element from a stylesheet rather than from text.
118
+ */
119
+ const style = (...rules: Array<CssChild>): StyleElement => ({ tag: 'style', rules });
120
+
121
+ const isStyle = (child: MarkupElement | StyleElement): child is StyleElement => 'rules' in child;
122
+
123
+ /** Drops the children that carry nothing and flattens the arrays. */
124
+ const flatten = (children: Array<Child>, into: Array<FlatChild> = []): Array<FlatChild> => {
125
+ for (const child of children) {
126
+ if (child === false || child === null || child === undefined || child === '') {
127
+ continue;
128
+ }
129
+ if (Array.isArray(child)) {
130
+ flatten(child, into);
131
+ } else {
132
+ into.push(child);
133
+ }
134
+ }
135
+ return into;
136
+ };
137
+
138
+ const openTag = (node: MarkupElement, selfClosing: boolean): string => {
139
+ let tag = `<${node.tag}`;
140
+ const { attributes } = node;
141
+
142
+ // `Object.keys` widens to `string`, so the cast restores what the type already says.
143
+ for (const name of Object.keys(attributes) as Array<Attribute>) {
144
+ const value = attributes[name];
145
+ if (value !== undefined) {
146
+ // A number can hold nothing that needs escaping, so it skips the scan.
147
+ tag += typeof value === 'number' ? ` ${name}="${value}"` : ` ${name}="${encodeHTML(value)}"`;
148
+ }
149
+ }
150
+
151
+ return `${tag}${selfClosing ? ' />' : '>'}`;
152
+ };
153
+
154
+ /** Serializes an element and its descendants onto one line, whitespace and all. */
155
+ const writeInline = (node: FlatChild): string => {
156
+ if (typeof node === 'string') {
157
+ return encodeHTML(node);
158
+ }
159
+ if (typeof node === 'number') {
160
+ return String(node);
161
+ }
162
+ if (isStyle(node)) {
163
+ return '';
164
+ }
165
+
166
+ const children = flatten(node.children);
167
+ if (children.length === 0) {
168
+ return openTag(node, true);
169
+ }
170
+
171
+ let inner = '';
172
+ for (const child of children) {
173
+ inner += writeInline(child);
174
+ }
175
+ return `${openTag(node, false)}${inner}</${node.tag}>`;
176
+ };
177
+
178
+ /**
179
+ * A container is written optimistically and rewound if its content turns out empty,
180
+ * so an element that draws nothing costs one walk rather than two.
181
+ */
182
+ const write = (node: FlatChild, depth: number, sink: Sink): void => {
183
+ const indent = indentAt(depth);
184
+
185
+ if (typeof node === 'string') {
186
+ sink.out += `${indent}${encodeHTML(node)}\n`;
187
+ return;
188
+ }
189
+ if (typeof node === 'number') {
190
+ sink.out += `${indent}${node}\n`;
191
+ return;
192
+ }
193
+
194
+ const mark = sink.out.length;
195
+
196
+ if (isStyle(node)) {
197
+ sink.out += `${indent}<style>\n`;
198
+ const bodyStart = sink.out.length;
199
+ writeCss(node.rules, depth + 1, sink);
200
+ sink.out =
201
+ sink.out.length === bodyStart ? sink.out.slice(0, mark) : `${sink.out}${indent}</style>\n`;
202
+ return;
203
+ }
204
+
205
+ const children = flatten(node.children);
206
+ if (children.length === 0) {
207
+ sink.out += `${indent}${openTag(node, true)}\n`;
208
+ return;
209
+ }
210
+
211
+ if (INLINE_ELEMENTS.has(node.tag)) {
212
+ let inner = '';
213
+ for (const child of children) {
214
+ inner += writeInline(child);
215
+ }
216
+ sink.out +=
217
+ inner === ''
218
+ ? `${indent}${openTag(node, true)}\n`
219
+ : `${indent}${openTag(node, false)}${inner}</${node.tag}>\n`;
220
+ return;
221
+ }
222
+
223
+ sink.out += `${indent}${openTag(node, false)}\n`;
224
+ const bodyStart = sink.out.length;
225
+ for (const child of children) {
226
+ write(child, depth + 1, sink);
227
+ }
228
+ sink.out =
229
+ sink.out.length === bodyStart
230
+ ? `${sink.out.slice(0, mark)}${indent}${openTag(node, true)}\n`
231
+ : `${sink.out}${indent}</${node.tag}>\n`;
232
+ };
233
+
234
+ /**
235
+ * Serializes a finished card: one element per line, two spaces per nesting level.
236
+ *
237
+ * @returns The rendered SVG.
238
+ */
239
+ const renderMarkup = (root: MarkupElement): string => {
240
+ const sink: Sink = { out: '' };
241
+ write(root, 0, sink);
242
+ return sink.out;
243
+ };
244
+
245
+ export { el, renderMarkup, style };
246
+ export type { Child, MarkupElement };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What has to be encoded: the markup characters, everything above plain ASCII, and a backspace.
3
+ * One source for both regexes, so the check and the replacement cannot disagree.
4
+ */
5
+ const ENCODED = String.raw`[<>&"'\u00A0-\u9999\u0008]`;
6
+
7
+ /** Non-global, so `.test` stays stateless: a global regex would carry `lastIndex`. */
8
+ const NEEDS_ENCODING = new RegExp(ENCODED);
9
+ const TO_ENCODE = new RegExp(ENCODED, 'g');
10
+
11
+ const BACKSPACE = '\u0008';
12
+
13
+ /**
14
+ * Encodes a string for use as markup, one numeric character reference each.
15
+ *
16
+ * The serializer is now the only place text becomes markup, so nothing arrives pre-escaped:
17
+ * a literal `&#38;` is encoded like any other `&` rather than passed through as a reference.
18
+ *
19
+ * @see https://stackoverflow.com/a/48073476/10629172
20
+ *
21
+ * @returns The text, safe to place in an attribute or as character data.
22
+ */
23
+ const encodeHTML = (str: string): string => {
24
+ // Most strings a card writes are plain ASCII, so they leave without a rewrite.
25
+ if (!NEEDS_ENCODING.test(str)) {
26
+ return str;
27
+ }
28
+
29
+ return str.replace(TO_ENCODE, (char) =>
30
+ char === BACKSPACE ? '' : `&#${char.codePointAt(0) ?? 0};`,
31
+ );
32
+ };
33
+
34
+ export { encodeHTML };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The markup layer: a card is built as a tree of elements and rules, then serialized once.
3
+ * It depends on nothing else in the package, so it sits below `cards/`, not inside `common/`.
4
+ */
5
+
6
+ export { atRule, cssComment, rule } from './css.ts';
7
+ export type { CssChild, Declarations } from './css.ts';
8
+ export { el, renderMarkup, style } from './element.ts';
9
+ export type { Child, MarkupElement } from './element.ts';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What the markup and stylesheet serializers share: where output goes, and how deep a line sits.
3
+ * Both write into one growing string rather than a list of chunks, so nothing needs joining.
4
+ */
5
+
6
+ const INDENT = ' ';
7
+
8
+ /** Collects serializer output. A plain holder so appends stay a property write. */
9
+ interface Sink {
10
+ out: string;
11
+ }
12
+
13
+ /** Indentation by depth, grown as the tree is walked rather than rebuilt per line. */
14
+ const indents = ['', INDENT];
15
+
16
+ /**
17
+ * @returns Two spaces per level.
18
+ */
19
+ const indentAt = (depth: number): string => {
20
+ for (let i = indents.length; i <= depth; i += 1) {
21
+ indents.push(`${indents[i - 1] ?? ''}${INDENT}`);
22
+ }
23
+ return indents[depth] ?? '';
24
+ };
25
+
26
+ export { INDENT, indentAt };
27
+ export type { Sink };
@@ -10,12 +10,12 @@ interface Theme {
10
10
  * Collection of available themes.
11
11
  */
12
12
  export const themes = {
13
+ // No `border_color`: it is derived from the background, so a custom `bg_color` keeps its own.
13
14
  default: {
14
15
  title_color: '2f80ed',
15
16
  icon_color: '4c71f2',
16
17
  text_color: '434d58',
17
18
  bg_color: 'fffefe',
18
- border_color: 'e4e2e2',
19
19
  },
20
20
  default_repocard: {
21
21
  title_color: '2f80ed',