@regmisatyam/retex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js ADDED
@@ -0,0 +1,802 @@
1
+ // src/theme/default.ts
2
+ var defaultTheme = {
3
+ name: "default",
4
+ colors: {
5
+ primary: "#2563eb",
6
+ secondary: "#64748b",
7
+ text: "#1e293b",
8
+ muted: "#64748b",
9
+ background: "#ffffff",
10
+ border: "#e2e8f0",
11
+ accent: "#2563eb",
12
+ success: "#16a34a"
13
+ },
14
+ fonts: {
15
+ heading: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
16
+ body: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
17
+ mono: '"JetBrains Mono", "SF Mono", "Fira Code", Consolas, monospace'
18
+ },
19
+ fontSizes: {
20
+ base: "10.5pt",
21
+ small: "9pt",
22
+ large: "12pt",
23
+ Large: "15pt",
24
+ Huge: "22pt",
25
+ name: "26pt",
26
+ section: "13pt"
27
+ },
28
+ spacing: {
29
+ unit: "4px",
30
+ section: "1.1rem",
31
+ item: "0.28rem",
32
+ page: "0.55in"
33
+ },
34
+ page: {
35
+ size: "Letter",
36
+ margin: "0.55in",
37
+ maxWidth: "8.5in"
38
+ },
39
+ sectionStyle: "rule"
40
+ };
41
+
42
+ // src/theme/themes.ts
43
+ function resolveTheme(partial, base = defaultTheme) {
44
+ if (!partial) return base;
45
+ return {
46
+ name: partial.name ?? base.name,
47
+ colors: { ...base.colors, ...partial.colors },
48
+ fonts: { ...base.fonts, ...partial.fonts },
49
+ fontSizes: { ...base.fontSizes, ...partial.fontSizes },
50
+ spacing: { ...base.spacing, ...partial.spacing },
51
+ page: { ...base.page, ...partial.page },
52
+ sectionStyle: partial.sectionStyle ?? base.sectionStyle
53
+ };
54
+ }
55
+ resolveTheme({
56
+ name: "modern",
57
+ colors: { primary: "#7c3aed", accent: "#7c3aed", text: "#111827" },
58
+ sectionStyle: "underline"
59
+ });
60
+ resolveTheme({
61
+ name: "classic",
62
+ colors: { primary: "#111827", secondary: "#4b5563", accent: "#111827" },
63
+ fonts: {
64
+ heading: 'Georgia, "Times New Roman", serif',
65
+ body: 'Georgia, "Times New Roman", serif',
66
+ mono: 'Consolas, "Courier New", monospace'
67
+ },
68
+ sectionStyle: "rule"
69
+ });
70
+ resolveTheme({
71
+ name: "compact",
72
+ fontSizes: {
73
+ base: "9.5pt",
74
+ small: "8pt",
75
+ large: "11pt",
76
+ Large: "13pt",
77
+ Huge: "18pt",
78
+ name: "20pt",
79
+ section: "11pt"
80
+ },
81
+ spacing: { unit: "3px", section: "0.7rem", item: "0.18rem", page: "0.4in" },
82
+ sectionStyle: "bar"
83
+ });
84
+
85
+ // src/security/sanitize.ts
86
+ var SAFE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:", "sms:"]);
87
+ var SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
88
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
89
+ function sanitizeUrl(input) {
90
+ const raw = String(input ?? "").trim();
91
+ const cleaned = raw.replace(CONTROL_CHARS_RE, "");
92
+ const match = SCHEME_RE.exec(cleaned);
93
+ if (!match) {
94
+ return { safe: cleaned, blocked: false };
95
+ }
96
+ const scheme = match[1].toLowerCase() + ":";
97
+ if (SAFE_PROTOCOLS.has(scheme)) {
98
+ return { safe: cleaned, blocked: false, scheme };
99
+ }
100
+ return { safe: "#", blocked: true, scheme };
101
+ }
102
+ function isSafeColor(value) {
103
+ const v = value.trim();
104
+ if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v)) return true;
105
+ if (/^(rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/i.test(v)) return true;
106
+ return CSS_NAMED_COLORS.has(v.toLowerCase());
107
+ }
108
+ function isSafeDimension(value) {
109
+ return /^-?\d*\.?\d+(px|pt|em|rem|ex|ch|vw|vh|vmin|vmax|cm|mm|in|pc|%|fr)?$/.test(
110
+ value.trim()
111
+ );
112
+ }
113
+ function sanitizeStyleValue(value) {
114
+ const v = value.trim();
115
+ if (/[<>;{}]/.test(v) || /expression|url\s*\(|javascript:/i.test(v)) {
116
+ return void 0;
117
+ }
118
+ return v;
119
+ }
120
+ var CSS_NAMED_COLORS = /* @__PURE__ */ new Set([
121
+ "black",
122
+ "silver",
123
+ "gray",
124
+ "grey",
125
+ "white",
126
+ "maroon",
127
+ "red",
128
+ "purple",
129
+ "fuchsia",
130
+ "green",
131
+ "lime",
132
+ "olive",
133
+ "yellow",
134
+ "navy",
135
+ "blue",
136
+ "teal",
137
+ "aqua",
138
+ "cyan",
139
+ "magenta",
140
+ "orange",
141
+ "pink",
142
+ "brown",
143
+ "gold",
144
+ "indigo",
145
+ "violet",
146
+ "tan",
147
+ "beige",
148
+ "ivory",
149
+ "coral",
150
+ "salmon",
151
+ "khaki",
152
+ "crimson",
153
+ "turquoise",
154
+ "lavender",
155
+ "plum",
156
+ "orchid",
157
+ "slateblue",
158
+ "slategray",
159
+ "steelblue",
160
+ "skyblue",
161
+ "royalblue",
162
+ "midnightblue",
163
+ "darkblue",
164
+ "darkgreen",
165
+ "darkred",
166
+ "darkgray",
167
+ "darkgrey",
168
+ "lightgray",
169
+ "lightgrey",
170
+ "lightblue",
171
+ "transparent",
172
+ "currentcolor",
173
+ "inherit"
174
+ ]);
175
+
176
+ // src/icons/icons.ts
177
+ var ICONS = {
178
+ github: {
179
+ body: '<path d="M12 1.5A10.5 10.5 0 0 0 8.7 22c.5.1.7-.2.7-.5v-2c-2.9.6-3.5-1.3-3.5-1.3-.5-1.2-1.2-1.5-1.2-1.5-.9-.6.1-.6.1-.6 1 .1 1.6 1 1.6 1 .9 1.6 2.4 1.1 3 .9.1-.7.4-1.1.7-1.4-2.3-.3-4.8-1.2-4.8-5.2 0-1.2.4-2.1 1-2.9-.1-.3-.5-1.3.1-2.7 0 0 .9-.3 2.8 1.1a9.6 9.6 0 0 1 5 0c1.9-1.4 2.8-1.1 2.8-1.1.6 1.4.2 2.4.1 2.7.7.8 1 1.7 1 2.9 0 4-2.5 4.9-4.8 5.2.4.3.7 1 .7 2v3c0 .3.2.6.7.5A10.5 10.5 0 0 0 12 1.5Z"/>'
180
+ },
181
+ linkedin: {
182
+ body: '<path d="M20.5 2h-17A1.5 1.5 0 0 0 2 3.5v17A1.5 1.5 0 0 0 3.5 22h17a1.5 1.5 0 0 0 1.5-1.5v-17A1.5 1.5 0 0 0 20.5 2ZM8 19H5v-9h3v9ZM6.5 8.7A1.7 1.7 0 1 1 6.5 5.3a1.7 1.7 0 0 1 0 3.4ZM19 19h-3v-4.7c0-1.1 0-2.6-1.6-2.6s-1.8 1.2-1.8 2.5V19h-3v-9h2.9v1.2h.04a3.2 3.2 0 0 1 2.9-1.6c3.1 0 3.7 2 3.7 4.7V19Z"/>'
183
+ },
184
+ email: {
185
+ body: '<path d="M3 5h18a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm.4 2 8.6 6 8.6-6H3.4ZM20 8.2l-7.4 5.2a1 1 0 0 1-1.2 0L4 8.2V17h16V8.2Z"/>',
186
+ aliases: ["mail", "envelope"]
187
+ },
188
+ phone: {
189
+ body: '<path d="M6.6 10.8a15.5 15.5 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25 11.4 11.4 0 0 0 3.6.58 1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1 11.4 11.4 0 0 0 .57 3.6 1 1 0 0 1-.25 1l-2.2 2.2Z"/>',
190
+ aliases: ["tel", "telephone"]
191
+ },
192
+ location: {
193
+ body: '<path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7Zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5Z"/>',
194
+ aliases: ["map", "pin", "marker", "geo"]
195
+ },
196
+ website: {
197
+ body: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.9 6h-2.9a15.7 15.7 0 0 0-1.3-3.4A8 8 0 0 1 18.9 8ZM12 4c.8 1.1 1.5 2.5 1.9 4h-3.8c.4-1.5 1.1-2.9 1.9-4ZM4.3 14a8 8 0 0 1 0-4h3.3a17.6 17.6 0 0 0 0 4H4.3Zm.8 2h2.9a15.7 15.7 0 0 0 1.3 3.4A8 8 0 0 1 5.1 16Zm2.9-8H5.1a8 8 0 0 1 4.2-3.4A15.7 15.7 0 0 0 8 8Zm4 12c-.8-1.1-1.5-2.5-1.9-4h3.8c-.4 1.5-1.1 2.9-1.9 4Zm2.3-6h-4.6a15.3 15.3 0 0 1 0-4h4.6a15.3 15.3 0 0 1 0 4Zm.4 5.4a15.7 15.7 0 0 0 1.3-3.4h2.9a8 8 0 0 1-4.2 3.4ZM16.4 14a17.6 17.6 0 0 0 0-4h3.3a8 8 0 0 1 0 4h-3.3Z"/>',
198
+ aliases: ["globe", "web", "link", "url"]
199
+ },
200
+ twitter: {
201
+ body: '<path d="M22 5.9c-.7.3-1.5.6-2.3.7a4 4 0 0 0 1.8-2.2c-.8.5-1.7.8-2.6 1a4 4 0 0 0-6.8 3.6A11.3 11.3 0 0 1 3.9 4.8a4 4 0 0 0 1.2 5.3c-.6 0-1.2-.2-1.8-.5a4 4 0 0 0 3.2 3.9c-.6.1-1.2.2-1.8.1a4 4 0 0 0 3.7 2.8A8 8 0 0 1 2 18.1 11.3 11.3 0 0 0 8.1 20c7.4 0 11.5-6.2 11.5-11.5v-.5c.8-.6 1.5-1.3 2-2.1Z"/>',
202
+ aliases: ["x"]
203
+ },
204
+ gitlab: {
205
+ body: '<path d="m21.9 13.1-1.1-3.4-2.2-6.8a.6.6 0 0 0-1.1 0l-2.2 6.8H8.7L6.5 2.9a.6.6 0 0 0-1.1 0L3.2 9.7l-1.1 3.4a1.2 1.2 0 0 0 .4 1.3l9.5 6.9 9.5-6.9a1.2 1.2 0 0 0 .4-1.3Z"/>'
206
+ },
207
+ stackoverflow: {
208
+ body: '<path d="M17 21v-6h2v8H4v-8h2v6h11ZM7 17h9v-2H7v2Zm.3-4.2 8.8 1.8.4-2-8.8-1.8-.4 2Zm1.2-4.4 8.1 3.8.8-1.8-8.1-3.8-.8 1.8Zm2.5-4 6.9 5.7 1.3-1.5-6.9-5.7-1.3 1.5ZM15.6 1l-1.6 1.2 5.3 7.2 1.6-1.2L15.6 1Z"/>',
209
+ aliases: ["stack-overflow", "so"]
210
+ },
211
+ scholar: {
212
+ body: '<path d="M12 2 1 9l11 7 9-5.7V17h2V9L12 2ZM5 14.2V18c0 1.7 3.1 3 7 3s7-1.3 7-3v-3.8l-7 4.4-7-4.4Z"/>',
213
+ aliases: ["google-scholar", "academic"]
214
+ },
215
+ orcid: {
216
+ body: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM8.5 7.6a1.1 1.1 0 1 1 0 2.2 1.1 1.1 0 0 1 0-2.2ZM9.5 17h-2v-6.2h2V17Zm2-6.2h3.3c2.2 0 3.6 1.5 3.6 3.1 0 1.8-1.4 3.1-3.6 3.1H11.5v-6.2Zm2 1.7v2.8h1.1c1.2 0 1.8-.6 1.8-1.4s-.6-1.4-1.8-1.4h-1.1Z"/>'
217
+ },
218
+ calendar: {
219
+ body: '<path d="M7 2v2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2H7ZM5 9h14v10H5V9Z"/>',
220
+ aliases: ["date"]
221
+ },
222
+ briefcase: {
223
+ body: '<path d="M9 3a2 2 0 0 0-2 2v1H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-3V5a2 2 0 0 0-2-2H9Zm0 3V5h6v1H9Z"/>',
224
+ aliases: ["work", "job"]
225
+ }
226
+ };
227
+ var ALIASES = {};
228
+ for (const [name, def] of Object.entries(ICONS)) {
229
+ for (const alias of def.aliases ?? []) ALIASES[alias] = name;
230
+ }
231
+ function resolveIconName(name) {
232
+ const key = name.trim().toLowerCase();
233
+ if (ICONS[key]) return key;
234
+ if (ALIASES[key]) return ALIASES[key];
235
+ return void 0;
236
+ }
237
+ function getIcon(name) {
238
+ const key = resolveIconName(name);
239
+ return key ? ICONS[key] : void 0;
240
+ }
241
+ function iconToSvg(name, opts = {}) {
242
+ const def = getIcon(name);
243
+ if (!def) return null;
244
+ const size = opts.size ?? "1em";
245
+ const dim = typeof size === "number" ? `${size}` : size;
246
+ const cls = opts.className ? ` class="${opts.className}"` : "";
247
+ const fillOrStroke = def.stroked ? 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"' : 'fill="currentColor"';
248
+ return `<svg${cls} width="${dim}" height="${dim}" viewBox="0 0 24 24" ${fillOrStroke} role="img" aria-hidden="true" focusable="false">${def.body}</svg>`;
249
+ }
250
+
251
+ // src/renderers/css.ts
252
+ var tokenSafe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
253
+ function themeToCss(theme, prefix = "retex") {
254
+ const p = prefix;
255
+ const colorVars = Object.entries(theme.colors).map(([k, v]) => ` --${p}-color-${tokenSafe(k)}: ${v};`).join("\n");
256
+ return `.${p}-resume {
257
+ ${colorVars}
258
+ --${p}-primary: ${theme.colors.primary};
259
+ --${p}-text: ${theme.colors.text};
260
+ --${p}-muted: ${theme.colors.muted};
261
+ --${p}-border: ${theme.colors.border};
262
+ --${p}-bg: ${theme.colors.background};
263
+ --${p}-font-heading: ${theme.fonts.heading};
264
+ --${p}-font-body: ${theme.fonts.body};
265
+ --${p}-font-mono: ${theme.fonts.mono};
266
+ --${p}-fs-base: ${theme.fontSizes.base};
267
+ --${p}-fs-small: ${theme.fontSizes.small};
268
+ --${p}-fs-large: ${theme.fontSizes.large};
269
+ --${p}-fs-Large: ${theme.fontSizes.Large};
270
+ --${p}-fs-Huge: ${theme.fontSizes.Huge};
271
+ --${p}-fs-name: ${theme.fontSizes.name};
272
+ --${p}-fs-section: ${theme.fontSizes.section};
273
+ --${p}-sp-section: ${theme.spacing.section};
274
+ --${p}-sp-item: ${theme.spacing.item};
275
+
276
+ box-sizing: border-box;
277
+ max-width: ${theme.page.maxWidth};
278
+ margin: 0 auto;
279
+ padding: ${theme.spacing.page};
280
+ background: var(--${p}-bg);
281
+ color: var(--${p}-text);
282
+ font-family: var(--${p}-font-body);
283
+ font-size: var(--${p}-fs-base);
284
+ line-height: 1.45;
285
+ }
286
+ .${p}-resume *, .${p}-resume *::before, .${p}-resume *::after { box-sizing: border-box; }
287
+
288
+ .${p}-header { margin-bottom: var(--${p}-sp-section); }
289
+ .${p}-name {
290
+ font-family: var(--${p}-font-heading);
291
+ font-size: var(--${p}-fs-name);
292
+ font-weight: 700;
293
+ margin: 0 0 0.1em;
294
+ color: var(--${p}-text);
295
+ letter-spacing: -0.01em;
296
+ }
297
+ .${p}-title {
298
+ font-size: var(--${p}-fs-large);
299
+ color: var(--${p}-primary);
300
+ margin: 0 0 0.45em;
301
+ font-weight: 500;
302
+ }
303
+ .${p}-contact {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ gap: 0.35rem 1rem;
307
+ font-size: var(--${p}-fs-small);
308
+ color: var(--${p}-muted);
309
+ }
310
+ .${p}-contact-item {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 0.3em;
314
+ color: inherit;
315
+ text-decoration: none;
316
+ }
317
+ .${p}-contact-item svg { opacity: 0.8; }
318
+
319
+ .${p}-section { margin-bottom: var(--${p}-sp-section); break-inside: avoid; }
320
+ .${p}-section-title {
321
+ font-family: var(--${p}-font-heading);
322
+ font-size: var(--${p}-fs-section);
323
+ font-weight: 700;
324
+ text-transform: uppercase;
325
+ letter-spacing: 0.06em;
326
+ color: var(--${p}-primary);
327
+ margin: 0 0 0.5em;
328
+ }
329
+ .${p}-section--rule .${p}-section-title { border-bottom: 2px solid var(--${p}-border); padding-bottom: 0.2em; }
330
+ .${p}-section--underline .${p}-section-title { text-decoration: underline; text-underline-offset: 3px; }
331
+ .${p}-section--bar .${p}-section-title {
332
+ border-left: 3px solid var(--${p}-primary);
333
+ padding-left: 0.5em;
334
+ border-bottom: none;
335
+ }
336
+ .${p}-subsection-title {
337
+ font-family: var(--${p}-font-heading);
338
+ font-size: var(--${p}-fs-large);
339
+ font-weight: 600;
340
+ margin: 0.6em 0 0.3em;
341
+ }
342
+
343
+ .${p}-entry { margin-bottom: 0.6rem; break-inside: avoid; }
344
+ .${p}-entry-row {
345
+ display: flex;
346
+ justify-content: space-between;
347
+ align-items: baseline;
348
+ gap: 1rem;
349
+ }
350
+ .${p}-entry-title { font-weight: 700; }
351
+ .${p}-entry-subtitle { color: var(--${p}-primary); font-weight: 500; }
352
+ .${p}-entry-dates, .${p}-entry-location {
353
+ color: var(--${p}-muted);
354
+ font-size: var(--${p}-fs-small);
355
+ white-space: nowrap;
356
+ }
357
+ .${p}-entry-body { margin-top: 0.15rem; }
358
+ .${p}-entry-body p { margin: 0.2em 0; }
359
+
360
+ .${p}-list { margin: 0.2rem 0 0.2rem 1.1rem; padding: 0; }
361
+ .${p}-list li { margin-bottom: var(--${p}-sp-item); padding-left: 0.15rem; }
362
+
363
+ .${p}-columns { display: flex; gap: 1.5rem; align-items: flex-start; }
364
+ .${p}-column { min-width: 0; }
365
+
366
+ .${p}-skills { display: flex; flex-wrap: wrap; gap: 0.4rem; list-style: none; margin: 0.2rem 0; padding: 0; }
367
+ .${p}-skill {
368
+ background: color-mix(in srgb, var(--${p}-primary) 12%, transparent);
369
+ color: var(--${p}-primary);
370
+ border-radius: 4px;
371
+ padding: 0.12rem 0.5rem;
372
+ font-size: var(--${p}-fs-small);
373
+ font-weight: 500;
374
+ white-space: nowrap;
375
+ }
376
+
377
+ .${p}-link { color: var(--${p}-primary); text-decoration: none; }
378
+ .${p}-link:hover { text-decoration: underline; }
379
+ .${p}-icon { display: inline-flex; vertical-align: -0.125em; }
380
+ .${p}-rule { border: none; border-top: 1px solid var(--${p}-border); margin: 0.6rem 0; }
381
+ .${p}-center { text-align: center; }
382
+ .${p}-scale-small { font-size: var(--${p}-fs-small); }
383
+ .${p}-scale-large { font-size: var(--${p}-fs-large); }
384
+ .${p}-scale-Large { font-size: var(--${p}-fs-Large); }
385
+ .${p}-scale-Huge { font-size: var(--${p}-fs-Huge); }
386
+ p.${p}-para { margin: 0 0 0.45em; }
387
+ p.${p}-para:last-child { margin-bottom: 0; }
388
+
389
+ @media print {
390
+ .${p}-resume { max-width: none; margin: 0; padding: 0; }
391
+ .${p}-section, .${p}-entry { break-inside: avoid; }
392
+ a { color: inherit; }
393
+ }`;
394
+ }
395
+
396
+ // src/renderers/context.ts
397
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
398
+ "section",
399
+ "job",
400
+ "education",
401
+ "project",
402
+ "skills",
403
+ "list",
404
+ "columns",
405
+ "rule"
406
+ ]);
407
+ function isBlockNode(node) {
408
+ if (BLOCK_TYPES.has(node.type)) return true;
409
+ if (node.type === "space") return node.axis === "vertical";
410
+ if (node.type === "command" && node.name === "center") return true;
411
+ return false;
412
+ }
413
+
414
+ // src/renderers/structure.ts
415
+ function toRegions(children) {
416
+ const regions = [{ nodes: [] }];
417
+ for (const node of children) {
418
+ if (node.type === "section" && node.level === 1) {
419
+ regions.push({ section: node, nodes: [] });
420
+ } else {
421
+ regions[regions.length - 1].nodes.push(node);
422
+ }
423
+ }
424
+ return regions;
425
+ }
426
+ var CONTACTISH = /* @__PURE__ */ new Set(["contact", "icon", "link", "url"]);
427
+ function splitPreamble(nodes) {
428
+ const name = nodes.find(
429
+ (n) => n.type === "contact" && n.field === "name"
430
+ );
431
+ const title = nodes.find(
432
+ (n) => n.type === "contact" && n.field === "title"
433
+ );
434
+ const contacts = nodes.filter(
435
+ (n) => CONTACTISH.has(n.type) && n !== name && n !== title
436
+ );
437
+ const other = nodes.filter(
438
+ (n) => !CONTACTISH.has(n.type) && n.type !== "parbreak" && !(n.type === "text" && n.value.trim() === "")
439
+ );
440
+ return { name, title, contacts, other };
441
+ }
442
+ function entryParts(fields, kind) {
443
+ let title;
444
+ let subtitle;
445
+ if (kind === "education") {
446
+ title = fields.school ?? fields.degree ?? "";
447
+ subtitle = fields.school ? fields.degree ?? "" : "";
448
+ } else if (kind === "project") {
449
+ title = fields.name ?? fields.title ?? "";
450
+ subtitle = fields.organization ?? fields.tech ?? "";
451
+ } else {
452
+ title = fields.title ?? fields.role ?? fields.name ?? "";
453
+ subtitle = fields.company ?? fields.organization ?? "";
454
+ }
455
+ return {
456
+ title,
457
+ subtitle,
458
+ dates: dateRange(fields.start, fields.end, fields.date),
459
+ location: fields.location ?? "",
460
+ url: fields.url ?? fields.link ?? ""
461
+ };
462
+ }
463
+ function dateRange(start, end, date) {
464
+ if (date) return date;
465
+ if (start && end) return `${start} \u2013 ${end}`;
466
+ return start ?? end ?? "";
467
+ }
468
+
469
+ // src/renderers/react.ts
470
+ var tokenSafe2 = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
471
+ var ReactRenderer = class {
472
+ constructor(options) {
473
+ this.key = 0;
474
+ if (typeof options.createElement !== "function") {
475
+ throw new TypeError(
476
+ "ReactRenderer requires a `createElement` factory (e.g. React.createElement)."
477
+ );
478
+ }
479
+ this.theme = options.theme ?? resolveTheme();
480
+ this.prefix = options.classPrefix ?? "retex";
481
+ this.h = options.createElement;
482
+ this.Fragment = options.Fragment ?? "div";
483
+ this.overrides = options.overrides ?? /* @__PURE__ */ new Map();
484
+ this.useHeader = options.header ?? true;
485
+ }
486
+ /** The stylesheet for the active theme. Render it in a `<style>` yourself. */
487
+ styles() {
488
+ return themeToCss(this.theme, this.prefix);
489
+ }
490
+ /** Render the document AST to a React element. */
491
+ render(ast) {
492
+ const regions = toRegions(ast.children);
493
+ const parts = [];
494
+ const preamble = regions[0]?.section ? void 0 : regions.shift();
495
+ if (preamble) parts.push(...this.renderPreamble(preamble.nodes));
496
+ for (const region of regions) {
497
+ if (region.section) parts.push(this.renderSection(region.section, region.nodes));
498
+ else parts.push(...this.renderFlow(region.nodes));
499
+ }
500
+ return this.el("div", { className: this.cls("resume") }, parts);
501
+ }
502
+ ctx() {
503
+ return {
504
+ theme: this.theme,
505
+ classPrefix: this.prefix,
506
+ h: this.h,
507
+ Fragment: this.Fragment,
508
+ overrides: this.overrides,
509
+ renderNodes: (nodes) => this.renderFlow(nodes),
510
+ cls: (name) => this.cls(name)
511
+ };
512
+ }
513
+ /* --------------------------- structuring ---------------------------- */
514
+ renderSection(section, body) {
515
+ return this.el(
516
+ "section",
517
+ {
518
+ className: `${this.cls("section")} ${this.cls("section")}--${this.theme.sectionStyle}`
519
+ },
520
+ [
521
+ this.el("h2", { className: this.cls("section-title") }, [section.title]),
522
+ this.el("div", { className: this.cls("section-body") }, this.renderFlow(body))
523
+ ]
524
+ );
525
+ }
526
+ renderPreamble(nodes) {
527
+ if (!this.useHeader) return this.renderFlow(nodes);
528
+ const { name, title, contacts, other } = splitPreamble(nodes);
529
+ const out = [];
530
+ if (name || title || contacts.length > 0) {
531
+ const head = [];
532
+ if (name) head.push(this.el("h1", { className: this.cls("name") }, [name.value]));
533
+ if (title) head.push(this.el("p", { className: this.cls("title") }, [title.value]));
534
+ if (contacts.length > 0) {
535
+ head.push(
536
+ this.el(
537
+ "div",
538
+ { className: this.cls("contact") },
539
+ contacts.map((c) => this.renderNode(c))
540
+ )
541
+ );
542
+ }
543
+ out.push(this.el("header", { className: this.cls("header") }, head));
544
+ }
545
+ if (other.length > 0) out.push(...this.renderFlow(other));
546
+ return out;
547
+ }
548
+ /* ----------------------------- flow / inline ------------------------ */
549
+ renderFlow(nodes) {
550
+ const out = [];
551
+ let inline = [];
552
+ const flush = () => {
553
+ if (inline.length === 0) return;
554
+ const meaningful = inline.some((n) => n.type !== "text" || n.value.trim() !== "");
555
+ if (meaningful) {
556
+ out.push(
557
+ this.el("p", { className: this.cls("para") }, this.renderInline(inline))
558
+ );
559
+ }
560
+ inline = [];
561
+ };
562
+ for (const node of nodes) {
563
+ if (node.type === "parbreak") flush();
564
+ else if (isBlockNode(node)) {
565
+ flush();
566
+ out.push(this.renderNode(node));
567
+ } else inline.push(node);
568
+ }
569
+ flush();
570
+ return out;
571
+ }
572
+ renderInline(nodes) {
573
+ return nodes.map((n) => this.renderNode(n));
574
+ }
575
+ /* ------------------------------ per node ---------------------------- */
576
+ renderNode(node) {
577
+ const override = this.overrides.get(node.type) ?? (node.type === "command" ? this.overrides.get(`command:${node.name}`) : void 0);
578
+ if (override) return override(node, this.ctx(), (ns) => this.renderFlow(ns));
579
+ switch (node.type) {
580
+ case "text":
581
+ return node.value;
582
+ case "parbreak":
583
+ return null;
584
+ case "linebreak":
585
+ return this.el("br", null, []);
586
+ case "rule":
587
+ return this.el("hr", { className: this.cls("rule") }, []);
588
+ case "space":
589
+ return node.axis === "vertical" ? this.el("div", { style: { height: this.dim(node.size) } }, []) : this.el(
590
+ "span",
591
+ { style: { display: "inline-block", width: this.dim(node.size) } },
592
+ []
593
+ );
594
+ case "group":
595
+ return this.frag(this.renderInline(node.children));
596
+ case "bold":
597
+ return this.el("strong", null, this.renderInline(node.children));
598
+ case "italic":
599
+ return this.el("em", null, this.renderInline(node.children));
600
+ case "underline":
601
+ return this.el("u", null, this.renderInline(node.children));
602
+ case "strike":
603
+ return this.el("s", null, this.renderInline(node.children));
604
+ case "color": {
605
+ const inner = this.renderInline(node.children);
606
+ if (!node.color || !isSafeColor(node.color)) return this.frag(inner);
607
+ return this.el("span", { style: { color: node.color } }, inner);
608
+ }
609
+ case "themecolor":
610
+ return this.el(
611
+ "span",
612
+ {
613
+ style: {
614
+ color: `var(--${this.prefix}-color-${tokenSafe2(node.token)}, currentColor)`
615
+ }
616
+ },
617
+ this.renderInline(node.children)
618
+ );
619
+ case "fontsize": {
620
+ const inner = this.renderInline(node.children);
621
+ if (!isSafeDimension(node.size)) return this.frag(inner);
622
+ return this.el("span", { style: { fontSize: node.size } }, inner);
623
+ }
624
+ case "fontfamily": {
625
+ const inner = this.renderInline(node.children);
626
+ const family = node.family === "monospace" ? `var(--${this.prefix}-font-mono)` : sanitizeStyleValue(node.family);
627
+ if (!family) return this.frag(inner);
628
+ return this.el("span", { style: { fontFamily: family } }, inner);
629
+ }
630
+ case "fontscale":
631
+ return node.scale === "normal" ? this.frag(this.renderInline(node.children)) : this.el(
632
+ "span",
633
+ { className: this.cls(`scale-${node.scale}`) },
634
+ this.renderInline(node.children)
635
+ );
636
+ case "link":
637
+ return this.renderLink(node.href, this.renderInline(node.children));
638
+ case "url":
639
+ return this.renderLink(node.href, [node.rawHref]);
640
+ case "icon":
641
+ return this.renderIcon(node.name);
642
+ case "section":
643
+ return this.el(
644
+ `h${node.level + 1}`,
645
+ { className: this.cls("subsection-title") },
646
+ [node.title]
647
+ );
648
+ case "list":
649
+ return this.renderList(node);
650
+ case "columns":
651
+ return this.renderColumns(node);
652
+ case "skills":
653
+ return this.el(
654
+ "ul",
655
+ { className: this.cls("skills") },
656
+ node.items.map((s) => this.el("li", { className: this.cls("skill") }, [s]))
657
+ );
658
+ case "job":
659
+ return this.renderEntry(node, "job");
660
+ case "education":
661
+ return this.renderEntry(node, "education");
662
+ case "project":
663
+ return this.renderEntry(node, "project");
664
+ case "contact":
665
+ return this.renderContact(node);
666
+ case "command":
667
+ return this.renderCommand(node);
668
+ default:
669
+ return null;
670
+ }
671
+ }
672
+ renderContact(node) {
673
+ const item = this.cls("contact-item");
674
+ switch (node.field) {
675
+ case "email":
676
+ return this.contactLink(`mailto:${node.value}`, "email", node.value, item);
677
+ case "phone":
678
+ return this.contactLink(
679
+ `tel:${node.value.replace(/[^\d+]/g, "")}`,
680
+ "phone",
681
+ node.value,
682
+ item
683
+ );
684
+ case "website":
685
+ return this.contactLink(node.value, "website", node.value, item);
686
+ case "location":
687
+ return this.el("span", { className: item }, [
688
+ this.renderIcon("location"),
689
+ node.value
690
+ ]);
691
+ default:
692
+ return this.el("span", { className: item }, [node.value]);
693
+ }
694
+ }
695
+ contactLink(url, icon, label, cls) {
696
+ const { safe } = sanitizeUrl(url);
697
+ return this.el("a", { className: cls, href: safe }, [this.renderIcon(icon), label]);
698
+ }
699
+ renderLink(href, label) {
700
+ const { safe } = sanitizeUrl(href);
701
+ const external = /^https?:/i.test(safe);
702
+ const props = { className: this.cls("link"), href: safe };
703
+ if (external) {
704
+ props.target = "_blank";
705
+ props.rel = "noopener noreferrer";
706
+ }
707
+ return this.el("a", props, label);
708
+ }
709
+ renderList(node) {
710
+ const tag = node.kind === "enumerate" ? "ol" : "ul";
711
+ return this.el(
712
+ tag,
713
+ { className: this.cls("list") },
714
+ node.items.map((item) => this.el("li", null, this.renderInline(item.children)))
715
+ );
716
+ }
717
+ renderColumns(node) {
718
+ return this.el(
719
+ "div",
720
+ { className: this.cls("columns") },
721
+ node.columns.map((col) => {
722
+ const basis = isSafeDimension(col.width) ? col.width : "auto";
723
+ const style = basis === "auto" ? { flex: "1 1 0" } : { flex: `0 0 ${basis}` };
724
+ return this.el(
725
+ "div",
726
+ { className: this.cls("column"), style },
727
+ this.renderFlow(col.children)
728
+ );
729
+ })
730
+ );
731
+ }
732
+ renderEntry(node, kind) {
733
+ const { title, subtitle, dates, location, url } = entryParts(node.fields, kind);
734
+ const rows = [];
735
+ const titleEl = url ? this.renderLink(sanitizeUrl(url).safe, [title]) : title;
736
+ rows.push(
737
+ this.el("div", { className: this.cls("entry-row") }, [
738
+ this.el("span", { className: this.cls("entry-title") }, [titleEl]),
739
+ dates ? this.el("span", { className: this.cls("entry-dates") }, [dates]) : null
740
+ ])
741
+ );
742
+ if (subtitle || location) {
743
+ rows.push(
744
+ this.el("div", { className: this.cls("entry-row") }, [
745
+ this.el("span", { className: this.cls("entry-subtitle") }, [subtitle]),
746
+ location ? this.el("span", { className: this.cls("entry-location") }, [location]) : null
747
+ ])
748
+ );
749
+ }
750
+ if (node.children.length > 0) {
751
+ rows.push(
752
+ this.el(
753
+ "div",
754
+ { className: this.cls("entry-body") },
755
+ this.renderFlow(node.children)
756
+ )
757
+ );
758
+ }
759
+ return this.el("div", { className: `${this.cls("entry")} ${this.cls(kind)}` }, rows);
760
+ }
761
+ renderCommand(node) {
762
+ const inner = node.args.flatMap((a) => a.children);
763
+ if (node.name === "center") {
764
+ return this.el("div", { className: this.cls("center") }, this.renderFlow(inner));
765
+ }
766
+ return this.frag(this.renderInline(inner));
767
+ }
768
+ renderIcon(name) {
769
+ const svg = iconToSvg(name);
770
+ if (!svg) return this.el("span", { className: this.cls("icon"), title: name }, []);
771
+ return this.el(
772
+ "span",
773
+ {
774
+ className: this.cls("icon"),
775
+ dangerouslySetInnerHTML: { __html: svg }
776
+ },
777
+ []
778
+ );
779
+ }
780
+ /* ------------------------------ helpers ----------------------------- */
781
+ el(type, props, children) {
782
+ const filtered = children.filter((c) => c !== null && c !== void 0 && c !== "");
783
+ const finalProps = { ...props ?? {}, key: this.key++ };
784
+ return this.h(type, finalProps, ...filtered);
785
+ }
786
+ frag(children) {
787
+ return this.el(this.Fragment, null, children);
788
+ }
789
+ dim(value) {
790
+ return isSafeDimension(value) ? value : "0";
791
+ }
792
+ cls(name) {
793
+ return `${this.prefix}-${name}`;
794
+ }
795
+ };
796
+ function renderReact(ast, options) {
797
+ return new ReactRenderer(options).render(ast);
798
+ }
799
+
800
+ export { ReactRenderer, renderReact };
801
+ //# sourceMappingURL=react.js.map
802
+ //# sourceMappingURL=react.js.map