@effing/canvas 0.22.3 → 0.23.1

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,
@@ -410,14 +644,7 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
410
644
  fontStyle,
411
645
  ctx
412
646
  );
413
- let baselineY;
414
- if (fontMetrics) {
415
- const typoAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
416
- const typoDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
417
- baselineY = totalHeight + (lineHeightPx + typoAscent - typoDescent) / 2;
418
- } else {
419
- baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
420
- }
647
+ const baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
421
648
  segments.push({
422
649
  text: line,
423
650
  x,
@@ -432,7 +659,9 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
432
659
  ascent: metrics.ascent,
433
660
  textDecoration,
434
661
  letterSpacing,
435
- lineIndex: i
662
+ lineIndex: i,
663
+ textStrokeWidth,
664
+ textStrokeColor
436
665
  });
437
666
  totalHeight += lineHeightPx;
438
667
  maxLineWidth = Math.max(maxLineWidth, lineWidth);
@@ -2168,14 +2397,42 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2168
2397
  ctx.fillStyle = seg.color;
2169
2398
  const x = offsetX + seg.x;
2170
2399
  const y = offsetY + seg.y;
2400
+ const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
2171
2401
  if (emojiStyle) {
2172
- await drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle);
2402
+ await drawSegmentWithEmoji(
2403
+ ctx,
2404
+ seg,
2405
+ x,
2406
+ y,
2407
+ textShadow,
2408
+ emojiStyle,
2409
+ hasStroke
2410
+ );
2173
2411
  } else if (seg.letterSpacing && seg.letterSpacing !== 0) {
2412
+ if (hasStroke) {
2413
+ drawStrokeWithLetterSpacing(
2414
+ ctx,
2415
+ seg.text,
2416
+ x,
2417
+ y,
2418
+ seg.letterSpacing,
2419
+ seg.textStrokeWidth,
2420
+ seg.textStrokeColor ?? seg.color
2421
+ );
2422
+ }
2174
2423
  drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
2175
2424
  } else {
2176
2425
  if (textShadow) {
2177
2426
  drawTextShadow(ctx, seg.text, x, y, textShadow);
2178
2427
  }
2428
+ if (hasStroke) {
2429
+ ctx.save();
2430
+ ctx.lineWidth = seg.textStrokeWidth;
2431
+ ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
2432
+ ctx.lineJoin = "round";
2433
+ ctx.strokeText(seg.text, x, y);
2434
+ ctx.restore();
2435
+ }
2179
2436
  ctx.fillText(seg.text, x, y);
2180
2437
  }
2181
2438
  if (seg.textDecoration) {
@@ -2183,7 +2440,7 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
2183
2440
  }
2184
2441
  }
2185
2442
  }
2186
- async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2443
+ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle, hasStroke) {
2187
2444
  const letterSpacing = seg.letterSpacing ?? 0;
2188
2445
  const runs = splitTextIntoRuns(
2189
2446
  seg.text,
@@ -2199,6 +2456,26 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
2199
2456
  if (textShadow) {
2200
2457
  drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
2201
2458
  }
2459
+ if (hasStroke) {
2460
+ if (letterSpacing !== 0) {
2461
+ drawStrokeWithLetterSpacing(
2462
+ ctx,
2463
+ run.text,
2464
+ x + run.x,
2465
+ y,
2466
+ letterSpacing,
2467
+ seg.textStrokeWidth,
2468
+ seg.textStrokeColor ?? seg.color
2469
+ );
2470
+ } else {
2471
+ ctx.save();
2472
+ ctx.lineWidth = seg.textStrokeWidth;
2473
+ ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
2474
+ ctx.lineJoin = "round";
2475
+ ctx.strokeText(run.text, x + run.x, y);
2476
+ ctx.restore();
2477
+ }
2478
+ }
2202
2479
  if (letterSpacing !== 0) {
2203
2480
  drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
2204
2481
  } else {
@@ -2224,6 +2501,19 @@ function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
2224
2501
  currentX += metrics.width + letterSpacing;
2225
2502
  }
2226
2503
  }
2504
+ function drawStrokeWithLetterSpacing(ctx, text, x, y, letterSpacing, strokeWidth, strokeColor) {
2505
+ ctx.save();
2506
+ ctx.lineWidth = strokeWidth;
2507
+ ctx.strokeStyle = strokeColor;
2508
+ ctx.lineJoin = "round";
2509
+ let currentX = x;
2510
+ for (const char of text) {
2511
+ ctx.strokeText(char, currentX, y);
2512
+ const metrics = ctx.measureText(char);
2513
+ currentX += metrics.width + letterSpacing;
2514
+ }
2515
+ ctx.restore();
2516
+ }
2227
2517
  function drawTextShadow(ctx, text, x, y, shadow) {
2228
2518
  const parts = shadow.match(
2229
2519
  /(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(.*)/
@@ -2783,6 +3073,20 @@ function expandStyle(raw, fontFamilies) {
2783
3073
  }
2784
3074
  delete style.textBox;
2785
3075
  }
3076
+ if (style.WebkitTextStroke !== void 0) {
3077
+ const val = String(style.WebkitTextStroke).trim();
3078
+ const match = val.match(/^(\S+)\s+(.+)$/);
3079
+ if (match) {
3080
+ if (style.WebkitTextStrokeWidth === void 0)
3081
+ style.WebkitTextStrokeWidth = parseValue(match[1]);
3082
+ if (style.WebkitTextStrokeColor === void 0)
3083
+ style.WebkitTextStrokeColor = match[2];
3084
+ } else {
3085
+ if (style.WebkitTextStrokeWidth === void 0)
3086
+ style.WebkitTextStrokeWidth = parseValue(val);
3087
+ }
3088
+ delete style.WebkitTextStroke;
3089
+ }
2786
3090
  if (typeof style.fontFamily === "string") {
2787
3091
  const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
2788
3092
  if (fontFamilies) {
@@ -2796,226 +3100,6 @@ function expandStyle(raw, fontFamilies) {
2796
3100
  return style;
2797
3101
  }
2798
3102
 
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
3103
  // src/jsx/yoga.ts
3020
3104
  import Yoga, {
3021
3105
  Align,
@@ -3399,14 +3483,22 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3399
3483
  const rawChildren = props.children;
3400
3484
  if (rawChildren !== void 0 && rawChildren !== null) {
3401
3485
  const childArray = Array.isArray(rawChildren) ? rawChildren.flat(Infinity) : [rawChildren];
3486
+ const ws = style.whiteSpace;
3487
+ const collapsesWhitespace = ws === void 0 || ws === "normal" || ws === "nowrap" || ws === "pre-line";
3488
+ let prevWasBr = false;
3402
3489
  for (const child of childArray) {
3403
3490
  if (child === null || child === void 0 || typeof child === "boolean")
3404
3491
  continue;
3492
+ let processedChild = child;
3493
+ if (prevWasBr && collapsesWhitespace && typeof child === "string") {
3494
+ processedChild = child.trimStart();
3495
+ }
3496
+ prevWasBr = isBrElement(child);
3405
3497
  const childYogaNode = createYogaNode();
3406
3498
  yogaNode.insertChild(childYogaNode, children.length);
3407
3499
  children.push(
3408
3500
  await buildNode(
3409
- child,
3501
+ processedChild,
3410
3502
  style,
3411
3503
  childYogaNode,
3412
3504
  viewportWidth,
@@ -3443,6 +3535,11 @@ function extractLayout(node, yogaNode) {
3443
3535
  height: layout.height
3444
3536
  };
3445
3537
  }
3538
+ function isBrElement(child) {
3539
+ if (child === null || child === void 0 || typeof child !== "object")
3540
+ return false;
3541
+ return child.type === "br";
3542
+ }
3446
3543
  function extractTextContent(children) {
3447
3544
  if (children === void 0 || children === null) return void 0;
3448
3545
  if (typeof children === "string") return children;