@effing/canvas 0.22.3 → 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/README.md CHANGED
@@ -192,24 +192,27 @@ const png = canvas.encodeSync("png");
192
192
 
193
193
  ### Typography
194
194
 
195
- | Property | Values / Notes |
196
- | ---------------- | ------------------------------------------------------------- |
197
- | `fontFamily` | Font name (inherited) |
198
- | `fontSize` | Number or CSS length (inherited) |
199
- | `fontWeight` | Numeric weight (inherited) |
200
- | `fontStyle` | `normal`, `italic` (inherited) |
201
- | `textAlign` | `left`, `center`, `right`, `justify` (inherited) |
202
- | `textDecoration` | String (inherited) |
203
- | `textTransform` | `none`, `uppercase`, `lowercase`, `capitalize` (inherited) |
204
- | `lineHeight` | Number or string (inherited) |
205
- | `letterSpacing` | Number or CSS length (inherited) |
206
- | `whiteSpace` | `normal`, `nowrap`, `pre`, `pre-wrap`, `pre-line` (inherited) |
207
- | `wordBreak` | `normal`, `break-all`, `break-word`, `keep-all` (inherited) |
208
- | `textOverflow` | `clip`, `ellipsis` (inherited) |
209
- | `lineClamp` | Number — max visible lines (adds ellipsis) |
210
- | `textBox` | Shorthand for `textBoxTrim` and `textBoxEdge` |
211
- | `textBoxTrim` | `none`, `trim-start`, `trim-end`, `trim-both` (inherited) |
212
- | `textBoxEdge` | e.g. `"cap alphabetic"` (inherited) |
195
+ | Property | Values / Notes |
196
+ | ----------------------- | ------------------------------------------------------------- |
197
+ | `fontFamily` | Font name (inherited) |
198
+ | `fontSize` | Number or CSS length (inherited) |
199
+ | `fontWeight` | Numeric weight (inherited) |
200
+ | `fontStyle` | `normal`, `italic` (inherited) |
201
+ | `textAlign` | `left`, `center`, `right`, `justify` (inherited) |
202
+ | `textDecoration` | String (inherited) |
203
+ | `textTransform` | `none`, `uppercase`, `lowercase`, `capitalize` (inherited) |
204
+ | `lineHeight` | Number or string (inherited) |
205
+ | `letterSpacing` | Number or CSS length (inherited) |
206
+ | `whiteSpace` | `normal`, `nowrap`, `pre`, `pre-wrap`, `pre-line` (inherited) |
207
+ | `wordBreak` | `normal`, `break-all`, `break-word`, `keep-all` (inherited) |
208
+ | `textOverflow` | `clip`, `ellipsis` (inherited) |
209
+ | `lineClamp` | Number — max visible lines (adds ellipsis) |
210
+ | `textBox` | Shorthand for `textBoxTrim` and `textBoxEdge` |
211
+ | `textBoxTrim` | `none`, `trim-start`, `trim-end`, `trim-both` (inherited) |
212
+ | `textBoxEdge` | e.g. `"cap alphabetic"` (inherited) |
213
+ | `WebkitTextStroke` | Shorthand, e.g. `"2px red"` (inherited) |
214
+ | `WebkitTextStrokeWidth` | CSS length (inherited) |
215
+ | `WebkitTextStrokeColor` | Any CSS color (inherited) |
213
216
 
214
217
  ### Effects
215
218
 
package/dist/index.js CHANGED
@@ -23,6 +23,229 @@ 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
+
26
249
  // src/jsx/font.ts
27
250
  import { GlobalFonts } from "@napi-rs/canvas";
28
251
 
@@ -312,6 +535,17 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
312
535
  const wordBreak = style.wordBreak ?? "normal";
313
536
  const textOverflow = style.textOverflow ?? "clip";
314
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;
315
549
  const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
316
550
  word,
317
551
  fontSize,
@@ -432,7 +666,9 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
432
666
  ascent: metrics.ascent,
433
667
  textDecoration,
434
668
  letterSpacing,
435
- lineIndex: i
669
+ lineIndex: i,
670
+ textStrokeWidth,
671
+ textStrokeColor
436
672
  });
437
673
  totalHeight += lineHeightPx;
438
674
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
@@ -2168,14 +2404,42 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2168
2404
  ctx.fillStyle = seg.color;
2169
2405
  const x = offsetX + seg.x;
2170
2406
  const y = offsetY + seg.y;
2407
+ const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
2171
2408
  if (emojiStyle) {
2172
- 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
+ );
2173
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
+ }
2174
2430
  drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
2175
2431
  } else {
2176
2432
  if (textShadow) {
2177
2433
  drawTextShadow(ctx, seg.text, x, y, textShadow);
2178
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
+ }
2179
2443
  ctx.fillText(seg.text, x, y);
2180
2444
  }
2181
2445
  if (seg.textDecoration) {
@@ -2183,7 +2447,7 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2183
2447
  }
2184
2448
  }
2185
2449
  }
2186
- async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2450
+ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle, hasStroke) {
2187
2451
  const letterSpacing = seg.letterSpacing ?? 0;
2188
2452
  const runs = splitTextIntoRuns(
2189
2453
  seg.text,
@@ -2199,6 +2463,26 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2199
2463
  if (textShadow) {
2200
2464
  drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
2201
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
+ }
2202
2486
  if (letterSpacing !== 0) {
2203
2487
  drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
2204
2488
  } else {
@@ -2224,6 +2508,19 @@ function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
2224
2508
  currentX += metrics.width + letterSpacing;
2225
2509
  }
2226
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
+ }
2227
2524
  function drawTextShadow(ctx, text, x, y, shadow) {
2228
2525
  const parts = shadow.match(
2229
2526
  /(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(.*)/
@@ -2783,6 +3080,20 @@ function expandStyle(raw, fontFamilies) {
2783
3080
  }
2784
3081
  delete style.textBox;
2785
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
+ }
2786
3097
  if (typeof style.fontFamily === "string") {
2787
3098
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
2788
3099
  if (fontFamilies) {
@@ -2796,226 +3107,6 @@ function expandStyle(raw, fontFamilies) {
2796
3107
  return style;
2797
3108
  }
2798
3109
 
2799
- // src/jsx/style/compute.ts
2800
- var ROOT_FONT_SIZE = 16;
2801
- var DEFAULT_STYLE = {
2802
- display: "flex",
2803
- flexDirection: "row",
2804
- flexWrap: "nowrap",
2805
- flexGrow: 0,
2806
- flexShrink: 0,
2807
- alignItems: "stretch",
2808
- justifyContent: "flex-start",
2809
- position: "relative",
2810
- fontSize: ROOT_FONT_SIZE,
2811
- fontWeight: 400,
2812
- fontStyle: "normal",
2813
- color: "black",
2814
- lineHeight: "normal",
2815
- textAlign: "left",
2816
- whiteSpace: "normal",
2817
- wordBreak: "normal",
2818
- textOverflow: "clip",
2819
- opacity: 1,
2820
- overflow: "visible"
2821
- };
2822
- var INHERITABLE_PROPS = [
2823
- "color",
2824
- "fontSize",
2825
- "fontFamily",
2826
- "fontWeight",
2827
- "fontStyle",
2828
- "textAlign",
2829
- "textTransform",
2830
- "textDecoration",
2831
- "lineHeight",
2832
- "letterSpacing",
2833
- "whiteSpace",
2834
- "wordBreak",
2835
- "textOverflow",
2836
- "lineClamp",
2837
- "textBoxTrim",
2838
- "textBoxEdge"
2839
- ];
2840
- function resolveStyle(rawStyle, parentStyle) {
2841
- const style = { ...rawStyle };
2842
- for (const prop of INHERITABLE_PROPS) {
2843
- if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
2844
- style[prop] = parentStyle[prop];
2845
- }
2846
- }
2847
- if (typeof style.fontSize === "string") {
2848
- const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
2849
- const resolved = resolveUnit(
2850
- style.fontSize,
2851
- 0,
2852
- 0,
2853
- parentFontSize,
2854
- ROOT_FONT_SIZE
2855
- );
2856
- style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
2857
- }
2858
- if (typeof style.lineHeight === "string") {
2859
- const str = style.lineHeight;
2860
- if (str.endsWith("%")) {
2861
- style.lineHeight = parseFloat(str) / 100;
2862
- } else {
2863
- const parsed = parseFloat(str);
2864
- if (!isNaN(parsed)) {
2865
- style.lineHeight = parsed;
2866
- }
2867
- }
2868
- }
2869
- if (typeof style.letterSpacing === "string") {
2870
- const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
2871
- const resolved = resolveUnit(
2872
- style.letterSpacing,
2873
- 0,
2874
- 0,
2875
- currentFontSize,
2876
- ROOT_FONT_SIZE
2877
- );
2878
- if (typeof resolved === "number") {
2879
- style.letterSpacing = resolved;
2880
- }
2881
- }
2882
- return style;
2883
- }
2884
- var DIMENSION_PROPS = [
2885
- "width",
2886
- "height",
2887
- "minWidth",
2888
- "minHeight",
2889
- "maxWidth",
2890
- "maxHeight",
2891
- "top",
2892
- "right",
2893
- "bottom",
2894
- "left",
2895
- "marginTop",
2896
- "marginRight",
2897
- "marginBottom",
2898
- "marginLeft",
2899
- "paddingTop",
2900
- "paddingRight",
2901
- "paddingBottom",
2902
- "paddingLeft",
2903
- "rowGap",
2904
- "columnGap",
2905
- "flexBasis",
2906
- "borderTopWidth",
2907
- "borderRightWidth",
2908
- "borderBottomWidth",
2909
- "borderLeftWidth",
2910
- "borderTopLeftRadius",
2911
- "borderTopRightRadius",
2912
- "borderBottomRightRadius",
2913
- "borderBottomLeftRadius"
2914
- ];
2915
- function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
2916
- if (value.endsWith("%") || value === "auto") return value;
2917
- if (value.endsWith("vmin")) {
2918
- const n = parseFloat(value);
2919
- return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
2920
- }
2921
- if (value.endsWith("vmax")) {
2922
- const n = parseFloat(value);
2923
- return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
2924
- }
2925
- if (value.endsWith("vw")) {
2926
- const n = parseFloat(value);
2927
- return isNaN(n) ? value : n / 100 * viewportWidth;
2928
- }
2929
- if (value.endsWith("vh")) {
2930
- const n = parseFloat(value);
2931
- return isNaN(n) ? value : n / 100 * viewportHeight;
2932
- }
2933
- if (value.endsWith("rem")) {
2934
- const n = parseFloat(value);
2935
- return isNaN(n) ? value : n * rootFontSize;
2936
- }
2937
- if (value.endsWith("em")) {
2938
- const n = parseFloat(value);
2939
- return isNaN(n) ? value : n * fontSize;
2940
- }
2941
- if (value.endsWith("px")) {
2942
- const n = parseFloat(value);
2943
- return isNaN(n) ? value : n;
2944
- }
2945
- if (value.endsWith("pt")) {
2946
- const n = parseFloat(value);
2947
- return isNaN(n) ? value : n * (96 / 72);
2948
- }
2949
- if (value.endsWith("pc")) {
2950
- const n = parseFloat(value);
2951
- return isNaN(n) ? value : n * 16;
2952
- }
2953
- if (value.endsWith("in")) {
2954
- const n = parseFloat(value);
2955
- return isNaN(n) ? value : n * 96;
2956
- }
2957
- if (value.endsWith("cm")) {
2958
- const n = parseFloat(value);
2959
- return isNaN(n) ? value : n * (96 / 2.54);
2960
- }
2961
- if (value.endsWith("mm")) {
2962
- const n = parseFloat(value);
2963
- return isNaN(n) ? value : n * (96 / 25.4);
2964
- }
2965
- return value;
2966
- }
2967
- function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
2968
- const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
2969
- for (const prop of DIMENSION_PROPS) {
2970
- const value = style[prop];
2971
- if (typeof value !== "string") continue;
2972
- const resolved = resolveUnit(
2973
- value,
2974
- viewportWidth,
2975
- viewportHeight,
2976
- fontSize,
2977
- rootFontSize
2978
- );
2979
- if (resolved !== value) {
2980
- style[prop] = resolved;
2981
- }
2982
- }
2983
- if (style.transform) {
2984
- style.transform = resolveTransformUnits(
2985
- style.transform,
2986
- viewportWidth,
2987
- viewportHeight,
2988
- fontSize,
2989
- rootFontSize
2990
- );
2991
- }
2992
- if (style.transformOrigin) {
2993
- style.transformOrigin = resolveTransformUnits(
2994
- style.transformOrigin,
2995
- viewportWidth,
2996
- viewportHeight,
2997
- fontSize,
2998
- rootFontSize
2999
- );
3000
- }
3001
- return style;
3002
- }
3003
- function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
3004
- return transform.replace(
3005
- /(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
3006
- (match) => {
3007
- const resolved = resolveUnit(
3008
- match,
3009
- viewportWidth,
3010
- viewportHeight,
3011
- fontSize,
3012
- rootFontSize
3013
- );
3014
- return typeof resolved === "number" ? String(resolved) : match;
3015
- }
3016
- );
3017
- }
3018
-
3019
3110
  // src/jsx/yoga.ts
3020
3111
  import Yoga, {
3021
3112
  Align,
@@ -3399,14 +3490,22 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3399
3490
  const rawChildren = props.children;
3400
3491
  if (rawChildren !== void 0 && rawChildren !== null) {
3401
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;
3402
3496
  for (const child of childArray) {
3403
3497
  if (child === null || child === void 0 || typeof child === "boolean")
3404
3498
  continue;
3499
+ let processedChild = child;
3500
+ if (prevWasBr && collapsesWhitespace && typeof child === "string") {
3501
+ processedChild = child.trimStart();
3502
+ }
3503
+ prevWasBr = isBrElement(child);
3405
3504
  const childYogaNode = createYogaNode();
3406
3505
  yogaNode.insertChild(childYogaNode, children.length);
3407
3506
  children.push(
3408
3507
  await buildNode(
3409
- child,
3508
+ processedChild,
3410
3509
  style,
3411
3510
  childYogaNode,
3412
3511
  viewportWidth,
@@ -3443,6 +3542,11 @@ function extractLayout(node, yogaNode) {
3443
3542
  height: layout.height
3444
3543
  };
3445
3544
  }
3545
+ function isBrElement(child) {
3546
+ if (child === null || child === void 0 || typeof child !== "object")
3547
+ return false;
3548
+ return child.type === "br";
3549
+ }
3446
3550
  function extractTextContent(children) {
3447
3551
  if (children === void 0 || children === null) return void 0;
3448
3552
  if (typeof children === "string") return children;