@effing/canvas 0.22.2 → 0.23.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/index.js CHANGED
@@ -23,6 +23,292 @@ function renderLottieFrame(ctx, animation, frame) {
23
23
  // src/jsx/draw/index.ts
24
24
  import { loadImage as loadImage3 } from "@napi-rs/canvas";
25
25
 
26
+ // src/jsx/style/compute.ts
27
+ var ROOT_FONT_SIZE = 16;
28
+ var DEFAULT_STYLE = {
29
+ display: "flex",
30
+ flexDirection: "row",
31
+ flexWrap: "nowrap",
32
+ flexGrow: 0,
33
+ flexShrink: 0,
34
+ alignItems: "stretch",
35
+ justifyContent: "flex-start",
36
+ position: "relative",
37
+ fontSize: ROOT_FONT_SIZE,
38
+ fontWeight: 400,
39
+ fontStyle: "normal",
40
+ color: "black",
41
+ lineHeight: "normal",
42
+ textAlign: "left",
43
+ whiteSpace: "normal",
44
+ wordBreak: "normal",
45
+ textOverflow: "clip",
46
+ opacity: 1,
47
+ overflow: "visible"
48
+ };
49
+ var INHERITABLE_PROPS = [
50
+ "color",
51
+ "fontSize",
52
+ "fontFamily",
53
+ "fontWeight",
54
+ "fontStyle",
55
+ "textAlign",
56
+ "textTransform",
57
+ "textDecoration",
58
+ "lineHeight",
59
+ "letterSpacing",
60
+ "whiteSpace",
61
+ "wordBreak",
62
+ "textOverflow",
63
+ "lineClamp",
64
+ "textBoxTrim",
65
+ "textBoxEdge",
66
+ "WebkitTextStrokeWidth",
67
+ "WebkitTextStrokeColor"
68
+ ];
69
+ function resolveStyle(rawStyle, parentStyle) {
70
+ const style = { ...rawStyle };
71
+ for (const prop of INHERITABLE_PROPS) {
72
+ if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
73
+ style[prop] = parentStyle[prop];
74
+ }
75
+ }
76
+ if (typeof style.fontSize === "string") {
77
+ const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
78
+ const resolved = resolveUnit(
79
+ style.fontSize,
80
+ 0,
81
+ 0,
82
+ parentFontSize,
83
+ ROOT_FONT_SIZE
84
+ );
85
+ style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
86
+ }
87
+ if (typeof style.lineHeight === "string") {
88
+ const str = style.lineHeight;
89
+ if (str.endsWith("%")) {
90
+ style.lineHeight = parseFloat(str) / 100;
91
+ } else {
92
+ const parsed = parseFloat(str);
93
+ if (!isNaN(parsed)) {
94
+ style.lineHeight = parsed;
95
+ }
96
+ }
97
+ }
98
+ if (typeof style.letterSpacing === "string") {
99
+ const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
100
+ const resolved = resolveUnit(
101
+ style.letterSpacing,
102
+ 0,
103
+ 0,
104
+ currentFontSize,
105
+ ROOT_FONT_SIZE
106
+ );
107
+ if (typeof resolved === "number") {
108
+ style.letterSpacing = resolved;
109
+ }
110
+ }
111
+ return style;
112
+ }
113
+ var DIMENSION_PROPS = [
114
+ "width",
115
+ "height",
116
+ "minWidth",
117
+ "minHeight",
118
+ "maxWidth",
119
+ "maxHeight",
120
+ "top",
121
+ "right",
122
+ "bottom",
123
+ "left",
124
+ "marginTop",
125
+ "marginRight",
126
+ "marginBottom",
127
+ "marginLeft",
128
+ "paddingTop",
129
+ "paddingRight",
130
+ "paddingBottom",
131
+ "paddingLeft",
132
+ "rowGap",
133
+ "columnGap",
134
+ "flexBasis",
135
+ "borderTopWidth",
136
+ "borderRightWidth",
137
+ "borderBottomWidth",
138
+ "borderLeftWidth",
139
+ "borderTopLeftRadius",
140
+ "borderTopRightRadius",
141
+ "borderBottomRightRadius",
142
+ "borderBottomLeftRadius",
143
+ "WebkitTextStrokeWidth"
144
+ ];
145
+ function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
146
+ if (value.endsWith("%") || value === "auto") return value;
147
+ if (value.endsWith("vmin")) {
148
+ const n = parseFloat(value);
149
+ return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
150
+ }
151
+ if (value.endsWith("vmax")) {
152
+ const n = parseFloat(value);
153
+ return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
154
+ }
155
+ if (value.endsWith("vw")) {
156
+ const n = parseFloat(value);
157
+ return isNaN(n) ? value : n / 100 * viewportWidth;
158
+ }
159
+ if (value.endsWith("vh")) {
160
+ const n = parseFloat(value);
161
+ return isNaN(n) ? value : n / 100 * viewportHeight;
162
+ }
163
+ if (value.endsWith("rem")) {
164
+ const n = parseFloat(value);
165
+ return isNaN(n) ? value : n * rootFontSize;
166
+ }
167
+ if (value.endsWith("em")) {
168
+ const n = parseFloat(value);
169
+ return isNaN(n) ? value : n * fontSize;
170
+ }
171
+ if (value.endsWith("px")) {
172
+ const n = parseFloat(value);
173
+ return isNaN(n) ? value : n;
174
+ }
175
+ if (value.endsWith("pt")) {
176
+ const n = parseFloat(value);
177
+ return isNaN(n) ? value : n * (96 / 72);
178
+ }
179
+ if (value.endsWith("pc")) {
180
+ const n = parseFloat(value);
181
+ return isNaN(n) ? value : n * 16;
182
+ }
183
+ if (value.endsWith("in")) {
184
+ const n = parseFloat(value);
185
+ return isNaN(n) ? value : n * 96;
186
+ }
187
+ if (value.endsWith("cm")) {
188
+ const n = parseFloat(value);
189
+ return isNaN(n) ? value : n * (96 / 2.54);
190
+ }
191
+ if (value.endsWith("mm")) {
192
+ const n = parseFloat(value);
193
+ return isNaN(n) ? value : n * (96 / 25.4);
194
+ }
195
+ return value;
196
+ }
197
+ function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
198
+ const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
199
+ for (const prop of DIMENSION_PROPS) {
200
+ const value = style[prop];
201
+ if (typeof value !== "string") continue;
202
+ const resolved = resolveUnit(
203
+ value,
204
+ viewportWidth,
205
+ viewportHeight,
206
+ fontSize,
207
+ rootFontSize
208
+ );
209
+ if (resolved !== value) {
210
+ style[prop] = resolved;
211
+ }
212
+ }
213
+ if (style.transform) {
214
+ style.transform = resolveTransformUnits(
215
+ style.transform,
216
+ viewportWidth,
217
+ viewportHeight,
218
+ fontSize,
219
+ rootFontSize
220
+ );
221
+ }
222
+ if (style.transformOrigin) {
223
+ style.transformOrigin = resolveTransformUnits(
224
+ style.transformOrigin,
225
+ viewportWidth,
226
+ viewportHeight,
227
+ fontSize,
228
+ rootFontSize
229
+ );
230
+ }
231
+ return style;
232
+ }
233
+ function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
234
+ return transform.replace(
235
+ /(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
236
+ (match) => {
237
+ const resolved = resolveUnit(
238
+ match,
239
+ viewportWidth,
240
+ viewportHeight,
241
+ fontSize,
242
+ rootFontSize
243
+ );
244
+ return typeof resolved === "number" ? String(resolved) : match;
245
+ }
246
+ );
247
+ }
248
+
249
+ // src/jsx/font.ts
250
+ import { GlobalFonts } from "@napi-rs/canvas";
251
+
252
+ // src/jsx/font-metrics.ts
253
+ function parseFontMetrics(data) {
254
+ const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
255
+ const view = new DataView(buf);
256
+ if (buf.byteLength < 12) return null;
257
+ const numTables = view.getUint16(4);
258
+ if (buf.byteLength < 12 + numTables * 16) return null;
259
+ let headOffset = -1;
260
+ let os2Offset = -1;
261
+ for (let i = 0; i < numTables; i++) {
262
+ const recordOffset = 12 + i * 16;
263
+ const tag = String.fromCharCode(view.getUint8(recordOffset)) + String.fromCharCode(view.getUint8(recordOffset + 1)) + String.fromCharCode(view.getUint8(recordOffset + 2)) + String.fromCharCode(view.getUint8(recordOffset + 3));
264
+ const offset = view.getUint32(recordOffset + 8);
265
+ if (tag === "head") headOffset = offset;
266
+ else if (tag === "OS/2") os2Offset = offset;
267
+ }
268
+ if (headOffset < 0 || os2Offset < 0) return null;
269
+ if (buf.byteLength < headOffset + 20) return null;
270
+ const unitsPerEm = view.getUint16(headOffset + 18);
271
+ if (buf.byteLength < os2Offset + 74) return null;
272
+ const sTypoAscender = view.getInt16(os2Offset + 68);
273
+ const sTypoDescender = view.getInt16(os2Offset + 70);
274
+ const sTypoLineGap = view.getInt16(os2Offset + 72);
275
+ return { unitsPerEm, sTypoAscender, sTypoDescender, sTypoLineGap };
276
+ }
277
+
278
+ // src/jsx/font.ts
279
+ var registeredFonts = /* @__PURE__ */ new Set();
280
+ var metricsCache = /* @__PURE__ */ new Map();
281
+ function registerFont(font) {
282
+ const key = `${font.name}:${font.weight}:${font.style}`;
283
+ if (registeredFonts.has(key)) return;
284
+ const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
285
+ GlobalFonts.register(buffer, font.name);
286
+ const metrics = parseFontMetrics(font.data);
287
+ if (metrics) {
288
+ metricsCache.set(key, metrics);
289
+ }
290
+ registeredFonts.add(key);
291
+ }
292
+ function getFontMetrics(family, weight, style) {
293
+ const exact = metricsCache.get(`${family}:${weight}:${style}`);
294
+ if (exact) return exact;
295
+ for (const [key, metrics] of metricsCache) {
296
+ if (key.startsWith(`${family}:`)) return metrics;
297
+ }
298
+ return null;
299
+ }
300
+ function registerFontFromPath(path, nameAlias) {
301
+ GlobalFonts.registerFromPath(path, nameAlias ?? "");
302
+ }
303
+ function registeredFamilies() {
304
+ return GlobalFonts.families.map((f) => f.family);
305
+ }
306
+ function ensureFontsRegistered(fonts) {
307
+ for (const font of fonts) {
308
+ registerFont(font);
309
+ }
310
+ }
311
+
26
312
  // src/jsx/language.ts
27
313
  function isEmoji(char) {
28
314
  const cp = char.codePointAt(0);
@@ -110,12 +396,19 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
110
396
  height: ascent + descent
111
397
  };
112
398
  }
113
- function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx) {
399
+ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
114
400
  const c = ctx ?? getScratchCtx();
115
401
  setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
116
402
  const refMetrics = c.measureText("M");
117
- const fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
118
- const fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
403
+ let fontAscent;
404
+ let fontDescent;
405
+ if (fontMetrics) {
406
+ fontAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
407
+ fontDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
408
+ } else {
409
+ fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
410
+ fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
411
+ }
119
412
  const parts = edge.trim().split(/\s+/);
120
413
  const overEdge = parts[0] ?? "text";
121
414
  const underEdge = parts.length > 1 ? parts[1] : overEdge;
@@ -230,16 +523,29 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
230
523
  fontStyle,
231
524
  ctx
232
525
  );
526
+ const fontMetrics = getFontMetrics(fontFamily, fontWeight, fontStyle);
233
527
  const lineHeightPx = resolveLineHeight(
234
528
  style.lineHeight,
235
529
  fontSize,
236
- refMetrics
530
+ refMetrics,
531
+ fontMetrics
237
532
  );
238
533
  const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
239
534
  const whiteSpace = style.whiteSpace ?? "normal";
240
535
  const wordBreak = style.wordBreak ?? "normal";
241
536
  const textOverflow = style.textOverflow ?? "clip";
242
537
  const textDecoration = style.textDecoration;
538
+ let textStrokeWidth;
539
+ const rawStrokeWidth = style.WebkitTextStrokeWidth;
540
+ if (rawStrokeWidth !== void 0) {
541
+ if (typeof rawStrokeWidth === "number") {
542
+ textStrokeWidth = rawStrokeWidth;
543
+ } else {
544
+ const resolved = resolveUnit(String(rawStrokeWidth), 0, 0, fontSize, 16);
545
+ textStrokeWidth = typeof resolved === "number" ? resolved : void 0;
546
+ }
547
+ }
548
+ const textStrokeColor = style.WebkitTextStrokeColor;
243
549
  const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
244
550
  word,
245
551
  fontSize,
@@ -338,10 +644,18 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
338
644
  fontStyle,
339
645
  ctx
340
646
  );
647
+ let baselineY;
648
+ if (fontMetrics) {
649
+ const typoAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
650
+ const typoDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
651
+ baselineY = totalHeight + (lineHeightPx + typoAscent - typoDescent) / 2;
652
+ } else {
653
+ baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
654
+ }
341
655
  segments.push({
342
656
  text: line,
343
657
  x,
344
- y: totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2,
658
+ y: baselineY,
345
659
  width: lineWidth,
346
660
  height: lineHeightPx,
347
661
  fontSize,
@@ -352,7 +666,9 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
352
666
  ascent: metrics.ascent,
353
667
  textDecoration,
354
668
  letterSpacing,
355
- lineIndex: i
669
+ lineIndex: i,
670
+ textStrokeWidth,
671
+ textStrokeColor
356
672
  });
357
673
  totalHeight += lineHeightPx;
358
674
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
@@ -367,7 +683,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
367
683
  fontStyle,
368
684
  lineHeightPx,
369
685
  textBoxEdge,
370
- ctx
686
+ ctx,
687
+ fontMetrics
371
688
  );
372
689
  if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
373
690
  for (const seg of segments) {
@@ -385,9 +702,13 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
385
702
  height: totalHeight
386
703
  };
387
704
  }
388
- function resolveLineHeight(lineHeight, fontSize, metrics) {
705
+ function resolveLineHeight(lineHeight, fontSize, canvasMetrics, fontMetrics) {
389
706
  if (lineHeight === void 0 || lineHeight === "normal") {
390
- return metrics ? metrics.ascent + metrics.descent : fontSize * 1.2;
707
+ if (fontMetrics) {
708
+ const { sTypoAscender, sTypoDescender, sTypoLineGap, unitsPerEm } = fontMetrics;
709
+ return (sTypoAscender - sTypoDescender + sTypoLineGap) / unitsPerEm * fontSize;
710
+ }
711
+ return canvasMetrics ? canvasMetrics.ascent + canvasMetrics.descent : fontSize * 1.2;
391
712
  }
392
713
  if (typeof lineHeight === "number") {
393
714
  return lineHeight > 5 ? lineHeight : lineHeight * fontSize;
@@ -2083,14 +2404,42 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2083
2404
  ctx.fillStyle = seg.color;
2084
2405
  const x = offsetX + seg.x;
2085
2406
  const y = offsetY + seg.y;
2407
+ const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
2086
2408
  if (emojiStyle) {
2087
- await drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle);
2409
+ await drawSegmentWithEmoji(
2410
+ ctx,
2411
+ seg,
2412
+ x,
2413
+ y,
2414
+ textShadow,
2415
+ emojiStyle,
2416
+ hasStroke
2417
+ );
2088
2418
  } else if (seg.letterSpacing && seg.letterSpacing !== 0) {
2419
+ if (hasStroke) {
2420
+ drawStrokeWithLetterSpacing(
2421
+ ctx,
2422
+ seg.text,
2423
+ x,
2424
+ y,
2425
+ seg.letterSpacing,
2426
+ seg.textStrokeWidth,
2427
+ seg.textStrokeColor ?? seg.color
2428
+ );
2429
+ }
2089
2430
  drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
2090
2431
  } else {
2091
2432
  if (textShadow) {
2092
2433
  drawTextShadow(ctx, seg.text, x, y, textShadow);
2093
2434
  }
2435
+ if (hasStroke) {
2436
+ ctx.save();
2437
+ ctx.lineWidth = seg.textStrokeWidth;
2438
+ ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
2439
+ ctx.lineJoin = "round";
2440
+ ctx.strokeText(seg.text, x, y);
2441
+ ctx.restore();
2442
+ }
2094
2443
  ctx.fillText(seg.text, x, y);
2095
2444
  }
2096
2445
  if (seg.textDecoration) {
@@ -2098,7 +2447,7 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2098
2447
  }
2099
2448
  }
2100
2449
  }
2101
- async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2450
+ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle, hasStroke) {
2102
2451
  const letterSpacing = seg.letterSpacing ?? 0;
2103
2452
  const runs = splitTextIntoRuns(
2104
2453
  seg.text,
@@ -2114,6 +2463,26 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2114
2463
  if (textShadow) {
2115
2464
  drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
2116
2465
  }
2466
+ if (hasStroke) {
2467
+ if (letterSpacing !== 0) {
2468
+ drawStrokeWithLetterSpacing(
2469
+ ctx,
2470
+ run.text,
2471
+ x + run.x,
2472
+ y,
2473
+ letterSpacing,
2474
+ seg.textStrokeWidth,
2475
+ seg.textStrokeColor ?? seg.color
2476
+ );
2477
+ } else {
2478
+ ctx.save();
2479
+ ctx.lineWidth = seg.textStrokeWidth;
2480
+ ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
2481
+ ctx.lineJoin = "round";
2482
+ ctx.strokeText(run.text, x + run.x, y);
2483
+ ctx.restore();
2484
+ }
2485
+ }
2117
2486
  if (letterSpacing !== 0) {
2118
2487
  drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
2119
2488
  } else {
@@ -2139,6 +2508,19 @@ function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
2139
2508
  currentX += metrics.width + letterSpacing;
2140
2509
  }
2141
2510
  }
2511
+ function drawStrokeWithLetterSpacing(ctx, text, x, y, letterSpacing, strokeWidth, strokeColor) {
2512
+ ctx.save();
2513
+ ctx.lineWidth = strokeWidth;
2514
+ ctx.strokeStyle = strokeColor;
2515
+ ctx.lineJoin = "round";
2516
+ let currentX = x;
2517
+ for (const char of text) {
2518
+ ctx.strokeText(char, currentX, y);
2519
+ const metrics = ctx.measureText(char);
2520
+ currentX += metrics.width + letterSpacing;
2521
+ }
2522
+ ctx.restore();
2523
+ }
2142
2524
  function drawTextShadow(ctx, text, x, y, shadow) {
2143
2525
  const parts = shadow.match(
2144
2526
  /(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(.*)/
@@ -2524,28 +2906,6 @@ function parseAngle(value) {
2524
2906
  return parseFloat(value);
2525
2907
  }
2526
2908
 
2527
- // src/jsx/font.ts
2528
- import { GlobalFonts } from "@napi-rs/canvas";
2529
- var registeredFonts = /* @__PURE__ */ new Set();
2530
- function registerFont(font) {
2531
- const key = `${font.name}:${font.weight}:${font.style}`;
2532
- if (registeredFonts.has(key)) return;
2533
- const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
2534
- GlobalFonts.register(buffer, font.name);
2535
- registeredFonts.add(key);
2536
- }
2537
- function registerFontFromPath(path, nameAlias) {
2538
- GlobalFonts.registerFromPath(path, nameAlias ?? "");
2539
- }
2540
- function registeredFamilies() {
2541
- return GlobalFonts.families.map((f) => f.family);
2542
- }
2543
- function ensureFontsRegistered(fonts) {
2544
- for (const font of fonts) {
2545
- registerFont(font);
2546
- }
2547
- }
2548
-
2549
2909
  // src/jsx/layout.ts
2550
2910
  import { loadImage as loadImage4 } from "@napi-rs/canvas";
2551
2911
 
@@ -2720,6 +3080,20 @@ function expandStyle(raw, fontFamilies) {
2720
3080
  }
2721
3081
  delete style.textBox;
2722
3082
  }
3083
+ if (style.WebkitTextStroke !== void 0) {
3084
+ const val = String(style.WebkitTextStroke).trim();
3085
+ const match = val.match(/^(\S+)\s+(.+)$/);
3086
+ if (match) {
3087
+ if (style.WebkitTextStrokeWidth === void 0)
3088
+ style.WebkitTextStrokeWidth = parseValue(match[1]);
3089
+ if (style.WebkitTextStrokeColor === void 0)
3090
+ style.WebkitTextStrokeColor = match[2];
3091
+ } else {
3092
+ if (style.WebkitTextStrokeWidth === void 0)
3093
+ style.WebkitTextStrokeWidth = parseValue(val);
3094
+ }
3095
+ delete style.WebkitTextStroke;
3096
+ }
2723
3097
  if (typeof style.fontFamily === "string") {
2724
3098
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
2725
3099
  if (fontFamilies) {
@@ -2733,226 +3107,6 @@ function expandStyle(raw, fontFamilies) {
2733
3107
  return style;
2734
3108
  }
2735
3109
 
2736
- // src/jsx/style/compute.ts
2737
- var ROOT_FONT_SIZE = 16;
2738
- var DEFAULT_STYLE = {
2739
- display: "flex",
2740
- flexDirection: "row",
2741
- flexWrap: "nowrap",
2742
- flexGrow: 0,
2743
- flexShrink: 0,
2744
- alignItems: "stretch",
2745
- justifyContent: "flex-start",
2746
- position: "relative",
2747
- fontSize: ROOT_FONT_SIZE,
2748
- fontWeight: 400,
2749
- fontStyle: "normal",
2750
- color: "black",
2751
- lineHeight: "normal",
2752
- textAlign: "left",
2753
- whiteSpace: "normal",
2754
- wordBreak: "normal",
2755
- textOverflow: "clip",
2756
- opacity: 1,
2757
- overflow: "visible"
2758
- };
2759
- var INHERITABLE_PROPS = [
2760
- "color",
2761
- "fontSize",
2762
- "fontFamily",
2763
- "fontWeight",
2764
- "fontStyle",
2765
- "textAlign",
2766
- "textTransform",
2767
- "textDecoration",
2768
- "lineHeight",
2769
- "letterSpacing",
2770
- "whiteSpace",
2771
- "wordBreak",
2772
- "textOverflow",
2773
- "lineClamp",
2774
- "textBoxTrim",
2775
- "textBoxEdge"
2776
- ];
2777
- function resolveStyle(rawStyle, parentStyle) {
2778
- const style = { ...rawStyle };
2779
- for (const prop of INHERITABLE_PROPS) {
2780
- if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
2781
- style[prop] = parentStyle[prop];
2782
- }
2783
- }
2784
- if (typeof style.fontSize === "string") {
2785
- const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
2786
- const resolved = resolveUnit(
2787
- style.fontSize,
2788
- 0,
2789
- 0,
2790
- parentFontSize,
2791
- ROOT_FONT_SIZE
2792
- );
2793
- style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
2794
- }
2795
- if (typeof style.lineHeight === "string") {
2796
- const str = style.lineHeight;
2797
- if (str.endsWith("%")) {
2798
- style.lineHeight = parseFloat(str) / 100;
2799
- } else {
2800
- const parsed = parseFloat(str);
2801
- if (!isNaN(parsed)) {
2802
- style.lineHeight = parsed;
2803
- }
2804
- }
2805
- }
2806
- if (typeof style.letterSpacing === "string") {
2807
- const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
2808
- const resolved = resolveUnit(
2809
- style.letterSpacing,
2810
- 0,
2811
- 0,
2812
- currentFontSize,
2813
- ROOT_FONT_SIZE
2814
- );
2815
- if (typeof resolved === "number") {
2816
- style.letterSpacing = resolved;
2817
- }
2818
- }
2819
- return style;
2820
- }
2821
- var DIMENSION_PROPS = [
2822
- "width",
2823
- "height",
2824
- "minWidth",
2825
- "minHeight",
2826
- "maxWidth",
2827
- "maxHeight",
2828
- "top",
2829
- "right",
2830
- "bottom",
2831
- "left",
2832
- "marginTop",
2833
- "marginRight",
2834
- "marginBottom",
2835
- "marginLeft",
2836
- "paddingTop",
2837
- "paddingRight",
2838
- "paddingBottom",
2839
- "paddingLeft",
2840
- "rowGap",
2841
- "columnGap",
2842
- "flexBasis",
2843
- "borderTopWidth",
2844
- "borderRightWidth",
2845
- "borderBottomWidth",
2846
- "borderLeftWidth",
2847
- "borderTopLeftRadius",
2848
- "borderTopRightRadius",
2849
- "borderBottomRightRadius",
2850
- "borderBottomLeftRadius"
2851
- ];
2852
- function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
2853
- if (value.endsWith("%") || value === "auto") return value;
2854
- if (value.endsWith("vmin")) {
2855
- const n = parseFloat(value);
2856
- return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
2857
- }
2858
- if (value.endsWith("vmax")) {
2859
- const n = parseFloat(value);
2860
- return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
2861
- }
2862
- if (value.endsWith("vw")) {
2863
- const n = parseFloat(value);
2864
- return isNaN(n) ? value : n / 100 * viewportWidth;
2865
- }
2866
- if (value.endsWith("vh")) {
2867
- const n = parseFloat(value);
2868
- return isNaN(n) ? value : n / 100 * viewportHeight;
2869
- }
2870
- if (value.endsWith("rem")) {
2871
- const n = parseFloat(value);
2872
- return isNaN(n) ? value : n * rootFontSize;
2873
- }
2874
- if (value.endsWith("em")) {
2875
- const n = parseFloat(value);
2876
- return isNaN(n) ? value : n * fontSize;
2877
- }
2878
- if (value.endsWith("px")) {
2879
- const n = parseFloat(value);
2880
- return isNaN(n) ? value : n;
2881
- }
2882
- if (value.endsWith("pt")) {
2883
- const n = parseFloat(value);
2884
- return isNaN(n) ? value : n * (96 / 72);
2885
- }
2886
- if (value.endsWith("pc")) {
2887
- const n = parseFloat(value);
2888
- return isNaN(n) ? value : n * 16;
2889
- }
2890
- if (value.endsWith("in")) {
2891
- const n = parseFloat(value);
2892
- return isNaN(n) ? value : n * 96;
2893
- }
2894
- if (value.endsWith("cm")) {
2895
- const n = parseFloat(value);
2896
- return isNaN(n) ? value : n * (96 / 2.54);
2897
- }
2898
- if (value.endsWith("mm")) {
2899
- const n = parseFloat(value);
2900
- return isNaN(n) ? value : n * (96 / 25.4);
2901
- }
2902
- return value;
2903
- }
2904
- function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
2905
- const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
2906
- for (const prop of DIMENSION_PROPS) {
2907
- const value = style[prop];
2908
- if (typeof value !== "string") continue;
2909
- const resolved = resolveUnit(
2910
- value,
2911
- viewportWidth,
2912
- viewportHeight,
2913
- fontSize,
2914
- rootFontSize
2915
- );
2916
- if (resolved !== value) {
2917
- style[prop] = resolved;
2918
- }
2919
- }
2920
- if (style.transform) {
2921
- style.transform = resolveTransformUnits(
2922
- style.transform,
2923
- viewportWidth,
2924
- viewportHeight,
2925
- fontSize,
2926
- rootFontSize
2927
- );
2928
- }
2929
- if (style.transformOrigin) {
2930
- style.transformOrigin = resolveTransformUnits(
2931
- style.transformOrigin,
2932
- viewportWidth,
2933
- viewportHeight,
2934
- fontSize,
2935
- rootFontSize
2936
- );
2937
- }
2938
- return style;
2939
- }
2940
- function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
2941
- return transform.replace(
2942
- /(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
2943
- (match) => {
2944
- const resolved = resolveUnit(
2945
- match,
2946
- viewportWidth,
2947
- viewportHeight,
2948
- fontSize,
2949
- rootFontSize
2950
- );
2951
- return typeof resolved === "number" ? String(resolved) : match;
2952
- }
2953
- );
2954
- }
2955
-
2956
3110
  // src/jsx/yoga.ts
2957
3111
  import Yoga, {
2958
3112
  Align,
@@ -3336,14 +3490,22 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3336
3490
  const rawChildren = props.children;
3337
3491
  if (rawChildren !== void 0 && rawChildren !== null) {
3338
3492
  const childArray = Array.isArray(rawChildren) ? rawChildren.flat(Infinity) : [rawChildren];
3493
+ const ws = style.whiteSpace;
3494
+ const collapsesWhitespace = ws === void 0 || ws === "normal" || ws === "nowrap" || ws === "pre-line";
3495
+ let prevWasBr = false;
3339
3496
  for (const child of childArray) {
3340
3497
  if (child === null || child === void 0 || typeof child === "boolean")
3341
3498
  continue;
3499
+ let processedChild = child;
3500
+ if (prevWasBr && collapsesWhitespace && typeof child === "string") {
3501
+ processedChild = child.trimStart();
3502
+ }
3503
+ prevWasBr = isBrElement(child);
3342
3504
  const childYogaNode = createYogaNode();
3343
3505
  yogaNode.insertChild(childYogaNode, children.length);
3344
3506
  children.push(
3345
3507
  await buildNode(
3346
- child,
3508
+ processedChild,
3347
3509
  style,
3348
3510
  childYogaNode,
3349
3511
  viewportWidth,
@@ -3380,6 +3542,11 @@ function extractLayout(node, yogaNode) {
3380
3542
  height: layout.height
3381
3543
  };
3382
3544
  }
3545
+ function isBrElement(child) {
3546
+ if (child === null || child === void 0 || typeof child !== "object")
3547
+ return false;
3548
+ return child.type === "br";
3549
+ }
3383
3550
  function extractTextContent(children) {
3384
3551
  if (children === void 0 || children === null) return void 0;
3385
3552
  if (typeof children === "string") return children;