@effing/canvas 0.43.0 → 0.44.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
@@ -139,8 +139,22 @@ var INHERITABLE_PROPS = [
139
139
  "WebkitTextStrokeWidth",
140
140
  "WebkitTextStrokeColor"
141
141
  ];
142
+ var CSS_NUMBER = /^[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/;
143
+ var FONT_WEIGHT_KEYWORDS = /* @__PURE__ */ new Set(["normal", "bold", "bolder", "lighter"]);
144
+ function isValidFontWeight(weight) {
145
+ if (typeof weight === "string") {
146
+ const text = weight.trim();
147
+ if (FONT_WEIGHT_KEYWORDS.has(text.toLowerCase())) return true;
148
+ if (!CSS_NUMBER.test(text)) return false;
149
+ weight = Number(text);
150
+ }
151
+ return Number.isFinite(weight) && weight >= 1 && weight <= 1e3;
152
+ }
142
153
  function resolveStyle(rawStyle, parentStyle) {
143
154
  const style = { ...rawStyle };
155
+ if (style.fontWeight !== void 0 && !isValidFontWeight(style.fontWeight)) {
156
+ delete style.fontWeight;
157
+ }
144
158
  for (const prop of INHERITABLE_PROPS) {
145
159
  if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
146
160
  style[prop] = parentStyle[prop];
@@ -302,7 +316,9 @@ var LENGTH_STRING_PROPS = [
302
316
  ["transformOrigin", ""],
303
317
  ["clipPath", "px"],
304
318
  ["filter", "px"],
305
- ["backdropFilter", "px"]
319
+ ["backdropFilter", "px"],
320
+ ["boxShadow", "px"],
321
+ ["textShadow", "px"]
306
322
  ];
307
323
  function resolveLengthUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize, suffix = "") {
308
324
  return transform.replace(
@@ -386,7 +402,7 @@ function quoteFontFamilies(fontFamily) {
386
402
  function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
387
403
  ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${quoteFontFamilies(fontFamily)}`;
388
404
  }
389
- function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, fontAscent, fontDescent) {
405
+ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, line, edge) {
390
406
  const c = getScratchCtx();
391
407
  const measure = (text) => {
392
408
  setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
@@ -417,7 +433,7 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
417
433
  case "ideographic-ink":
418
434
  case "text":
419
435
  default:
420
- targetAscent = fontAscent;
436
+ targetAscent = line.ascent;
421
437
  break;
422
438
  }
423
439
  let targetDescent;
@@ -429,12 +445,11 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
429
445
  case "ideographic-ink":
430
446
  case "text":
431
447
  default:
432
- targetDescent = fontDescent;
448
+ targetDescent = line.descent;
433
449
  break;
434
450
  }
435
- const halfLeading = (lineHeight - (fontAscent + fontDescent)) / 2;
436
- const overTrim = halfLeading + (fontAscent - targetAscent);
437
- const underTrim = halfLeading + (fontDescent - targetDescent);
451
+ const overTrim = line.baseline - targetAscent;
452
+ const underTrim = line.lineHeight - line.baseline - targetDescent;
438
453
  return {
439
454
  overTrim: Math.max(0, overTrim),
440
455
  underTrim: Math.max(0, underTrim)
@@ -444,9 +459,10 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
444
459
  // src/jsx/text/native.ts
445
460
  function toWeight(weight) {
446
461
  if (typeof weight === "number") return weight;
447
- if (weight === "bold") return 700;
448
- const parsed = parseInt(String(weight ?? "400"), 10);
449
- return isNaN(parsed) ? 400 : parsed;
462
+ const text = String(weight ?? "400").trim().toLowerCase();
463
+ if (text === "bold") return 700;
464
+ const parsed = text === "" ? NaN : Number(text);
465
+ return Number.isFinite(parsed) ? parsed : 400;
450
466
  }
451
467
  function toFontStyle(fontStyle) {
452
468
  if (fontStyle?.startsWith("italic")) return "italic";
@@ -491,6 +507,17 @@ async function releaseParagraphs() {
491
507
  if (pendingParagraphs < MAX_PENDING_PARAGRAPHS) return;
492
508
  await new Promise((resolve) => setImmediate(resolve));
493
509
  }
510
+ function halfLeadingLineBox(lineHeight, ascent, descent) {
511
+ const a = Math.round(ascent);
512
+ const d = Math.round(descent);
513
+ const leading = (lineHeight - a - d) * 64;
514
+ return {
515
+ lineHeight,
516
+ baseline: a + Math.floor(Math.trunc(leading / 2) / 64),
517
+ ascent: a,
518
+ descent: d
519
+ };
520
+ }
494
521
  var graphemeSegmenter = new Intl.Segmenter(void 0, {
495
522
  granularity: "grapheme"
496
523
  });
@@ -562,6 +589,7 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
562
589
  fontWeight: toWeight(fontWeight),
563
590
  fontStyle: toFontStyle(fontStyle),
564
591
  letterSpacing,
592
+ // Undefined for `normal`, which the paragraph sizes as Chrome does.
565
593
  lineHeight,
566
594
  textAlign: toTextAlign(style.textAlign),
567
595
  noWrap,
@@ -574,12 +602,16 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
574
602
  });
575
603
  countParagraph();
576
604
  const layout = paragraph.layout(width);
605
+ const box = halfLeadingLineBox(
606
+ layout.lineHeight,
607
+ layout.ascent,
608
+ layout.descent
609
+ );
577
610
  const lines = layout.lines.length > 0 ? layout.lines : [
578
611
  {
579
612
  left: 0,
580
613
  width: 0,
581
- // As Skia places a baseline: by half-leading in the line box.
582
- baseline: (layout.lineHeight + layout.ascent - layout.descent) / 2,
614
+ baseline: box.baseline,
583
615
  startIndex: 0,
584
616
  endIndex: 0,
585
617
  hardBreak: true
@@ -587,8 +619,7 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
587
619
  ];
588
620
  const toTextIndex = content.toTextIndex ?? ((index) => index);
589
621
  const segments = lines.map((line, i) => ({
590
- // A line that ends the text at a newline reports the newline as its text.
591
- text: text.slice(toTextIndex(line.startIndex), toTextIndex(line.endIndex)).replace(/\n/g, ""),
622
+ text: text.slice(toTextIndex(line.startIndex), toTextIndex(line.endIndex)),
592
623
  x: line.left,
593
624
  y: line.baseline,
594
625
  width: line.width,
@@ -604,21 +635,17 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
604
635
  lineIndex: i
605
636
  }));
606
637
  const emoji = [];
607
- layout.placeholders.forEach((box, i) => {
608
- if (!box) return;
638
+ layout.placeholders.forEach((placed, i) => {
639
+ if (!placed) return;
609
640
  emoji.push({
610
641
  grapheme: content.emoji[i],
611
- x: box.x,
612
- y: box.y,
642
+ x: placed.x,
643
+ y: placed.y,
613
644
  size: fontSize,
614
- baseline: lines[box.line].baseline
645
+ baseline: lines[placed.line].baseline
615
646
  });
616
647
  });
617
648
  let height = lines.length * layout.lineHeight;
618
- if (lineHeight === void 0) {
619
- const noFontSize = Number.isFinite(styledFontSize) && styledFontSize <= 0;
620
- height = noFontSize ? 0 : Math.ceil(height);
621
- }
622
649
  let paragraphOffsetY = 0;
623
650
  const textBoxTrim = style.textBoxTrim;
624
651
  if (textBoxTrim && textBoxTrim !== "none") {
@@ -627,10 +654,8 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
627
654
  fontFamily,
628
655
  fontWeight,
629
656
  fontStyle,
630
- layout.lineHeight,
631
- style.textBoxEdge ?? "text",
632
- layout.ascent,
633
- layout.descent
657
+ box,
658
+ style.textBoxEdge ?? "text"
634
659
  );
635
660
  if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
636
661
  for (const seg of segments) seg.y -= trim.overTrim;
@@ -639,7 +664,7 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
639
664
  e.baseline -= trim.overTrim;
640
665
  }
641
666
  height -= trim.overTrim;
642
- paragraphOffsetY = -trim.overTrim;
667
+ paragraphOffsetY -= trim.overTrim;
643
668
  }
644
669
  if (textBoxTrim === "trim-end" || textBoxTrim === "trim-both") {
645
670
  height -= trim.underTrim;
@@ -649,6 +674,8 @@ function layoutTextNative(text, style, maxWidth, lineHeight, emojiEnabled) {
649
674
  segments,
650
675
  width: segments.reduce((w, s) => Math.max(w, s.width), 0),
651
676
  height,
677
+ maxContentWidth: layout.maxIntrinsicWidth,
678
+ minContentWidth: layout.minIntrinsicWidth,
652
679
  paragraph,
653
680
  paragraphOffsetY,
654
681
  emoji
@@ -660,15 +687,17 @@ function isWhiteSpaceOnly(text) {
660
687
  return /^[ \t\n\r\f\v]*$/.test(text);
661
688
  }
662
689
  function collapseWhiteSpace(text, whiteSpace) {
690
+ const pieces = typeof text === "string" ? [text] : text;
691
+ return pieces.map((piece) => collapsePiece(piece, whiteSpace)).join("\n").replace(/\r\n/g, "\n").replace(FINAL_BREAK, "").replace(SEPARATORS, SEPARATOR_SPACE);
692
+ }
693
+ function collapsePiece(text, whiteSpace) {
663
694
  let result = text.replace(/[\v\f]/g, "").replace(/\r\n/g, "\n");
664
- if (whiteSpace === "pre" || whiteSpace === "pre-wrap") {
665
- result = result.replace(/\r/g, "");
666
- } else if (/[\t\n\r]| {2}|^ | $/.test(result)) {
695
+ if (whiteSpace !== "pre" && whiteSpace !== "pre-wrap" && /[\t\n\r]| {2}|^ | $/.test(result)) {
667
696
  result = result.replace(/[\t\r]/g, " ").replace(/ *\n */g, "\n");
668
697
  if (whiteSpace !== "pre-line") result = result.replace(/\n+/g, " ");
669
698
  result = result.replace(/ {2,}/g, " ").replace(/^ | $/g, "");
670
699
  }
671
- return result.replace(FINAL_BREAK, "").replace(SEPARATORS, SEPARATOR_SPACE);
700
+ return result;
672
701
  }
673
702
  var FINAL_BREAK = /\n$/;
674
703
  var SEPARATORS = /[\u2028\u2029]/g;
@@ -716,57 +745,189 @@ function resolveLineHeight(lineHeight, fontSize) {
716
745
  }
717
746
  return Number.isFinite(px) && px >= 0 ? px : void 0;
718
747
  }
748
+ function toLayoutGrid(width) {
749
+ return Math.ceil(width * 64) / 64;
750
+ }
751
+ var graphemeSegmenter2 = new Intl.Segmenter(void 0, {
752
+ granularity: "grapheme"
753
+ });
719
754
  var TextMeasure = class {
720
755
  constructor(text, style, emojiEnabled) {
721
756
  this.text = text;
722
757
  this.style = style;
723
758
  this.emojiEnabled = emojiEnabled;
724
- this.measureStyle = { ...style, textOverflow: "clip" };
725
759
  }
726
760
  text;
727
761
  style;
728
762
  emojiEnabled;
729
- /** Yoga's measurements by width (Infinity for unbounded). */
763
+ /**
764
+ * Yoga's measurements by width (Infinity for unbounded), and those at
765
+ * exactly a width.
766
+ */
730
767
  sizes = /* @__PURE__ */ new Map();
731
- /** The heights reported to Yoga since the last `settle`. */
768
+ exactSizes = /* @__PURE__ */ new Map();
769
+ /**
770
+ * The heights reported to Yoga since the node was last marked dirty (see
771
+ * `settle`): Yoga may size the node, and its ancestors, from any of them,
772
+ * cached, without measuring it again.
773
+ */
732
774
  reported = /* @__PURE__ */ new Set();
733
775
  pinnedHeight;
734
776
  settled;
735
- measureStyle;
777
+ /**
778
+ * The text's intrinsic widths, from whichever layout came first: its
779
+ * max-content width, and a bound on its min-content width, which is the
780
+ * min-content width but where finding it takes more (see `widestGrapheme`).
781
+ */
782
+ intrinsic;
736
783
  /**
737
784
  * Measure the text for Yoga.
738
785
  *
739
786
  * @param maxWidth - The width available, Infinity for unbounded
787
+ * @param exact - Whether the node is exactly `maxWidth` wide, whatever its
788
+ * text: it's then measured for its height there
740
789
  */
741
- measure(maxWidth) {
742
- let size = this.sizes.get(maxWidth);
790
+ measure(maxWidth, exact = false) {
791
+ const sizes = exact ? this.exactSizes : this.sizes;
792
+ let size = sizes.get(maxWidth);
743
793
  if (!size) {
744
- const result = layoutText(
745
- this.text,
746
- this.measureStyle,
747
- maxWidth,
748
- this.emojiEnabled
749
- );
750
- const wrapped = result.segments.length > 1 && maxWidth < Infinity;
751
- size = {
752
- width: wrapped ? maxWidth : Math.min(result.width, maxWidth),
753
- height: result.height
754
- };
755
- this.sizes.set(maxWidth, size);
794
+ let result = this.layOut(maxWidth);
795
+ let width;
796
+ if (exact) {
797
+ width = maxWidth;
798
+ } else if (result.maxContentWidth > maxWidth) {
799
+ width = this.minContentBound <= maxWidth ? maxWidth : Math.max(maxWidth, this.minContentWidth);
800
+ if (width > maxWidth) result = this.layOut(width);
801
+ } else {
802
+ width = Math.min(
803
+ toLayoutGrid(Math.min(result.width, result.maxContentWidth)),
804
+ maxWidth
805
+ );
806
+ }
807
+ size = { width, height: result.height };
808
+ sizes.set(maxWidth, size);
756
809
  }
757
810
  const height = this.pinnedHeight ?? size.height;
758
811
  this.reported.add(height);
759
812
  return { width: size.width, height };
760
813
  }
814
+ /**
815
+ * The height of the text's lines at exactly `width`, as drawn there,
816
+ * whatever height later measurements are pinned to.
817
+ */
818
+ heightAt(width) {
819
+ if (this.settled?.width === width) return this.settled.result.height;
820
+ let size = this.exactSizes.get(width);
821
+ if (!size) {
822
+ size = { width, height: this.layOut(width).height };
823
+ this.exactSizes.set(width, size);
824
+ }
825
+ return size.height;
826
+ }
827
+ /**
828
+ * The text's min-content width: as narrow as it gets, each run of it that
829
+ * can't break on a line of its own. As CSS has it, that's the widest word,
830
+ * or the widest line for text that doesn't wrap, and under
831
+ * `overflow-wrap: anywhere` (or `word-break: break-word`) and
832
+ * `word-break: break-all` the widest letter. Text truncated with an
833
+ * ellipsis (`text-overflow: ellipsis` without wrapping, or a line clamp)
834
+ * has none: it's drawn at whatever width it has, which CSS gives it in
835
+ * the `overflow: hidden` box an ellipsis needs.
836
+ *
837
+ * Never wider than the text's widest line, and on the layout grid (see
838
+ * `toLayoutGrid`), as the widths `measure` reports are.
839
+ */
840
+ get minContentWidth() {
841
+ const intrinsic = this.intrinsic ?? this.measureIntrinsic();
842
+ intrinsic.min ??= Math.min(
843
+ toLayoutGrid(this.widestGrapheme()),
844
+ intrinsic.minBound
845
+ );
846
+ return intrinsic.min;
847
+ }
848
+ /** At least the text's min-content width, and found without more work. */
849
+ get minContentBound() {
850
+ return (this.intrinsic ?? this.measureIntrinsic()).minBound;
851
+ }
852
+ /**
853
+ * The text's max-content width: its widest line, were no line to wrap, on
854
+ * the layout grid.
855
+ */
856
+ get maxContentWidth() {
857
+ return (this.intrinsic ?? this.measureIntrinsic()).max;
858
+ }
859
+ /** Whether the text can't be narrower than its widest line. */
860
+ get unbreakable() {
861
+ return this.minContentBound >= this.maxContentWidth;
862
+ }
863
+ measureIntrinsic() {
864
+ this.layOut(Infinity);
865
+ return this.intrinsic;
866
+ }
867
+ layOut(width) {
868
+ const result = layoutText(this.text, this.style, width, this.emojiEnabled);
869
+ this.intrinsic ??= this.intrinsicOf(result);
870
+ return result;
871
+ }
872
+ /** The intrinsic widths of the text, from a layout of it at any width. */
873
+ intrinsicOf(result) {
874
+ const { style } = this;
875
+ const max = toLayoutGrid(result.maxContentWidth);
876
+ const noWrap = style.whiteSpace === "nowrap" || style.whiteSpace === "pre";
877
+ if (style.lineClamp !== void 0 && style.lineClamp >= 1 || noWrap && style.textOverflow === "ellipsis") {
878
+ return { max, minBound: 0, min: 0 };
879
+ }
880
+ const minBound = Math.min(
881
+ toLayoutGrid(
882
+ noWrap ? Math.min(result.minContentWidth, result.width) : result.minContentWidth
883
+ ),
884
+ max
885
+ );
886
+ const anywhere = !noWrap && (style.overflowWrap === "anywhere" || style.wordBreak === "break-word");
887
+ return { max, minBound, min: anywhere ? void 0 : minBound };
888
+ }
889
+ /**
890
+ * The width of the text's widest grapheme cluster: its min-content where
891
+ * a line may break between any two. Each one is laid out once, as a word
892
+ * of its own.
893
+ */
894
+ widestGrapheme() {
895
+ const text = applyTextTransform(
896
+ collapseWhiteSpace(this.text, this.style.whiteSpace),
897
+ this.style
898
+ );
899
+ const graphemes = new Set(
900
+ /^[\s!-\u02ff]*$/.test(text) ? text : Array.from(graphemeSegmenter2.segment(text), ({ segment }) => segment)
901
+ );
902
+ for (const grapheme of graphemes) {
903
+ if (/^\s+$/.test(grapheme)) graphemes.delete(grapheme);
904
+ }
905
+ if (graphemes.size === 0) return 0;
906
+ return layoutText(
907
+ [...graphemes].join(" "),
908
+ {
909
+ ...this.style,
910
+ whiteSpace: "normal",
911
+ wordBreak: "normal",
912
+ overflowWrap: "normal",
913
+ textTransform: "none"
914
+ },
915
+ Infinity,
916
+ this.emojiEnabled
917
+ ).minContentWidth;
918
+ }
761
919
  /**
762
920
  * Lay the text out for drawing at the node's final content width, and
763
- * check it against the heights Yoga was given since the last call.
921
+ * check it against the heights Yoga was given since the node was last
922
+ * marked dirty: Yoga reuses a node's measurements until then, also when
923
+ * the node ends up at another width, so the heights of earlier layouts
924
+ * count too, not just those of the last.
764
925
  *
765
926
  * @param pin - Whether to pin later measurements to the drawn height when
766
927
  * they disagree
767
928
  * @returns Whether Yoga sized the node from another height. If `pin`, the
768
929
  * node's measurements are now pinned to the drawn height, and the caller
769
- * should mark the node dirty and compute the layout again.
930
+ * must mark the node dirty and compute the layout again.
770
931
  */
771
932
  settle(width, pin) {
772
933
  if (this.settled?.width !== width) {
@@ -777,20 +938,22 @@ var TextMeasure = class {
777
938
  }
778
939
  const { height } = this.settled.result;
779
940
  const agrees = this.reported.size === 0 || this.reported.size === 1 && this.reported.has(height);
780
- this.reported.clear();
781
941
  if (agrees) return false;
782
- if (pin) this.pinnedHeight = height;
942
+ if (pin) {
943
+ this.pinnedHeight = height;
944
+ this.reported.clear();
945
+ }
783
946
  return true;
784
947
  }
785
948
  /**
786
949
  * Drop the pinned height, so that Yoga sees the measured heights again.
787
950
  *
788
- * @returns Whether there was a pin
951
+ * @returns Whether there was a pin, for the caller to mark the node dirty
789
952
  */
790
953
  unpin() {
791
954
  const pinned = this.pinnedHeight !== void 0;
792
955
  this.pinnedHeight = void 0;
793
- this.reported.clear();
956
+ if (pinned) this.reported.clear();
794
957
  return pinned;
795
958
  }
796
959
  /** The text as laid out by the last `settle`. */
@@ -1050,34 +1213,114 @@ function drawBorders(ctx, x, y, width, height, style, borderRadius) {
1050
1213
  }
1051
1214
  }
1052
1215
  }
1053
- var BOX_SHADOW_RE = /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/;
1054
- function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
1055
- const parts = boxShadow.match(BOX_SHADOW_RE);
1056
- if (!parts) return;
1057
- const offsetX = parseFloat(parts[1]);
1058
- const offsetY = parseFloat(parts[2]);
1059
- const blur = parseFloat(parts[3]);
1060
- const color = parts[5].trim();
1061
- const radii = [
1062
- borderRadius.topLeft,
1063
- borderRadius.topRight,
1064
- borderRadius.bottomRight,
1065
- borderRadius.bottomLeft
1066
- ];
1067
- const margin = blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
1216
+ var NO_COLOR = "rgba(0, 0, 0, 0)";
1217
+ function drawBoxShadow(ctx, x, y, width, height, shadows, borderRadius) {
1218
+ const outer = shadows.filter((s) => !s.inset);
1219
+ if (outer.length === 0) return;
1220
+ const radii = clampRadii(borderRadius, width, height);
1221
+ let margin = 0;
1222
+ for (const s of outer) {
1223
+ margin = Math.max(margin, reach(s));
1224
+ }
1068
1225
  ctx.save();
1069
1226
  ctx.beginPath();
1070
1227
  ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
1071
- ctx.roundRect(x, y, width, height, radii);
1228
+ roundedRect(ctx, x, y, width, height, ...radii);
1072
1229
  ctx.clip("evenodd");
1073
- ctx.filter = `blur(${blur / 2}px)`;
1074
- ctx.translate(offsetX, offsetY);
1075
- ctx.fillStyle = color;
1230
+ for (let i = outer.length - 1; i >= 0; i--) {
1231
+ const s = outer[i];
1232
+ const w = width + s.spread * 2;
1233
+ const h = height + s.spread * 2;
1234
+ if (w <= 0 || h <= 0) continue;
1235
+ ctx.save();
1236
+ setShadowPaint(ctx, s);
1237
+ ctx.beginPath();
1238
+ roundedRect(
1239
+ ctx,
1240
+ x - s.spread + s.offsetX,
1241
+ y - s.spread + s.offsetY,
1242
+ w,
1243
+ h,
1244
+ ...spreadRadii(radii, s.spread)
1245
+ );
1246
+ ctx.fill();
1247
+ ctx.restore();
1248
+ }
1249
+ ctx.restore();
1250
+ }
1251
+ function drawInsetBoxShadow(ctx, x, y, width, height, shadows, borderRadius, style) {
1252
+ const inset = shadows.filter((s) => s.inset);
1253
+ if (inset.length === 0) return;
1254
+ const bt = resolveBoxValue(style.borderTopWidth, width);
1255
+ const br = resolveBoxValue(style.borderRightWidth, width);
1256
+ const bb = resolveBoxValue(style.borderBottomWidth, width);
1257
+ const bl = resolveBoxValue(style.borderLeftWidth, width);
1258
+ const px = x + bl;
1259
+ const py = y + bt;
1260
+ const pw = width - bl - br;
1261
+ const ph = height - bt - bb;
1262
+ if (pw <= 0 || ph <= 0) return;
1263
+ const [tl, tr, brr, bll] = clampRadii(borderRadius, width, height);
1264
+ const inner = [
1265
+ Math.max(0, tl - Math.max(bt, bl)),
1266
+ Math.max(0, tr - Math.max(bt, br)),
1267
+ Math.max(0, brr - Math.max(bb, br)),
1268
+ Math.max(0, bll - Math.max(bb, bl))
1269
+ ];
1270
+ ctx.save();
1076
1271
  ctx.beginPath();
1077
- ctx.roundRect(x, y, width, height, radii);
1078
- ctx.fill();
1272
+ roundedRect(ctx, px, py, pw, ph, ...inner);
1273
+ ctx.clip();
1274
+ for (let i = inset.length - 1; i >= 0; i--) {
1275
+ const s = inset[i];
1276
+ const m = reach(s);
1277
+ ctx.save();
1278
+ setShadowPaint(ctx, s);
1279
+ ctx.beginPath();
1280
+ ctx.rect(px - m, py - m, pw + m * 2, ph + m * 2);
1281
+ const w = pw - s.spread * 2;
1282
+ const h = ph - s.spread * 2;
1283
+ if (w > 0 && h > 0) {
1284
+ roundedRect(
1285
+ ctx,
1286
+ px + s.spread + s.offsetX,
1287
+ py + s.spread + s.offsetY,
1288
+ w,
1289
+ h,
1290
+ ...spreadRadii(inner, -s.spread)
1291
+ );
1292
+ }
1293
+ ctx.fill("evenodd");
1294
+ ctx.restore();
1295
+ }
1079
1296
  ctx.restore();
1080
1297
  }
1298
+ function reach(s) {
1299
+ return s.blur * 2 + Math.abs(s.offsetX) + Math.abs(s.offsetY) + Math.abs(s.spread) + 1;
1300
+ }
1301
+ function setShadowPaint(ctx, s) {
1302
+ ctx.fillStyle = NO_COLOR;
1303
+ ctx.fillStyle = s.color;
1304
+ if (s.blur > 0) ctx.filter = `blur(${s.blur / 2}px)`;
1305
+ }
1306
+ function clampRadii(r, width, height) {
1307
+ const max = maxCornerRadius(width, height);
1308
+ return [
1309
+ Math.min(r.topLeft, max),
1310
+ Math.min(r.topRight, max),
1311
+ Math.min(r.bottomRight, max),
1312
+ Math.min(r.bottomLeft, max)
1313
+ ];
1314
+ }
1315
+ function spreadRadii(radii, spread) {
1316
+ return radii.map((r) => spreadRadius(r, spread));
1317
+ }
1318
+ function spreadRadius(r, spread) {
1319
+ if (spread <= 0) return Math.max(0, r + spread);
1320
+ if (r >= spread) return r + spread;
1321
+ const ratio = r / spread;
1322
+ return r + spread * (1 + (ratio - 1) ** 3);
1323
+ }
1081
1324
 
1082
1325
  // src/jsx/draw/clip-path.ts
1083
1326
  var GEOMETRY_BOXES = /* @__PURE__ */ new Set([
@@ -1994,6 +2237,80 @@ async function drawImage(ctx, src, x, y, width, height, context, style) {
1994
2237
  }
1995
2238
  }
1996
2239
 
2240
+ // src/jsx/draw/shadow.ts
2241
+ var BOX_SHADOW = { maxLengths: 4, inset: true };
2242
+ var TEXT_SHADOW = { maxLengths: 3, inset: false };
2243
+ function parseBoxShadow(value, currentColor) {
2244
+ return parseShadowList(value, currentColor, BOX_SHADOW);
2245
+ }
2246
+ function parseTextShadow(value, currentColor) {
2247
+ return parseShadowList(value, currentColor, TEXT_SHADOW);
2248
+ }
2249
+ function parseShadowList(value, currentColor, syntax) {
2250
+ const text = value.trim();
2251
+ if (text === "" || text.toLowerCase() === "none") return [];
2252
+ const shadows = [];
2253
+ for (const item of splitTopLevel(text, ",")) {
2254
+ const shadow = parseShadow(item, currentColor, syntax);
2255
+ if (!shadow) return [];
2256
+ shadows.push(shadow);
2257
+ }
2258
+ return shadows;
2259
+ }
2260
+ var LENGTH = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(px)?$/i;
2261
+ var NUMERIC = /^[+-]?\.?\d/;
2262
+ function parseShadow(item, currentColor, syntax) {
2263
+ const tokens = splitTopLevel(item, " ");
2264
+ const lengths = [];
2265
+ let lengthsEnded = false;
2266
+ let inset = false;
2267
+ let color;
2268
+ for (const token of tokens) {
2269
+ if (NUMERIC.test(token)) {
2270
+ const match = LENGTH.exec(token);
2271
+ if (!match || lengthsEnded) return null;
2272
+ lengths.push(parseFloat(match[1]));
2273
+ continue;
2274
+ }
2275
+ if (lengths.length > 0) lengthsEnded = true;
2276
+ if (token.toLowerCase() === "inset") {
2277
+ if (!syntax.inset || inset) return null;
2278
+ inset = true;
2279
+ } else {
2280
+ if (color !== void 0) return null;
2281
+ color = token;
2282
+ }
2283
+ }
2284
+ if (lengths.length < 2 || lengths.length > syntax.maxLengths) return null;
2285
+ const [offsetX, offsetY, blur = 0, spread = 0] = lengths;
2286
+ if (blur < 0) return null;
2287
+ if (color === void 0 || color.toLowerCase() === "currentcolor") {
2288
+ color = currentColor;
2289
+ }
2290
+ return { offsetX, offsetY, blur, spread, inset, color };
2291
+ }
2292
+ function splitTopLevel(text, separator) {
2293
+ const parts = [];
2294
+ let depth = 0;
2295
+ let current = "";
2296
+ for (const ch of text) {
2297
+ if (ch === "(") depth++;
2298
+ else if (ch === ")") depth = Math.max(0, depth - 1);
2299
+ const splits = depth === 0 && (separator === "," ? ch === "," : /\s/.test(ch));
2300
+ if (splits) {
2301
+ parts.push(current);
2302
+ current = "";
2303
+ } else {
2304
+ current += ch;
2305
+ }
2306
+ }
2307
+ parts.push(current);
2308
+ if (separator === ",") {
2309
+ return parts.map((p) => p.trim());
2310
+ }
2311
+ return parts.filter((p) => p !== "");
2312
+ }
2313
+
1997
2314
  // src/jsx/draw/svg/style.ts
1998
2315
  function resolveCurrentColor(value, color) {
1999
2316
  if (value?.toLowerCase() === "currentcolor") return color;
@@ -3103,12 +3420,12 @@ async function drawText(ctx, layout, offsetX, offsetY, textShadow, emojiStyle) {
3103
3420
  if (!first) return;
3104
3421
  const x = offsetX;
3105
3422
  const y = offsetY + layout.paragraphOffsetY;
3106
- const shadow = textShadow ? parseShadow(textShadow) : null;
3423
+ const shadows = textShadow ? parseTextShadow(textShadow, first.color) : [];
3107
3424
  ctx.fillStyle = first.color;
3108
- if (shadow) {
3425
+ for (let i = shadows.length - 1; i >= 0; i--) {
3109
3426
  drawShadowPass(
3110
3427
  ctx,
3111
- shadow,
3428
+ shadows[i],
3112
3429
  getColorAlpha(first.color),
3113
3430
  () => fillParagraph(ctx, paragraph, x, y)
3114
3431
  );
@@ -3154,24 +3471,13 @@ async function drawEmoji(ctx, emoji, font, offsetX, offsetY, emojiStyle) {
3154
3471
  ctx.textRendering = textRendering;
3155
3472
  });
3156
3473
  }
3157
- function parseShadow(shadow) {
3158
- const parts = shadow.match(
3159
- /(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(.*)/
3160
- );
3161
- if (!parts) return null;
3162
- return {
3163
- offsetX: parseFloat(parts[1]),
3164
- offsetY: parseFloat(parts[2]),
3165
- blur: parseFloat(parts[3]),
3166
- color: parts[4].trim()
3167
- };
3168
- }
3169
3474
  function getColorAlpha(color) {
3170
3475
  const parsed = parseCssColor2(color);
3171
3476
  return parsed ? parsed.alpha : 1;
3172
3477
  }
3173
3478
  function drawShadowPass(ctx, shadow, textAlpha, drawFn) {
3174
3479
  ctx.save();
3480
+ ctx.fillStyle = "rgba(0, 0, 0, 0)";
3175
3481
  ctx.fillStyle = shadow.color;
3176
3482
  if (textAlpha < 1) ctx.globalAlpha *= textAlpha;
3177
3483
  if (shadow.blur > 0) ctx.filter = `blur(${shadow.blur / 2}px)`;
@@ -3261,9 +3567,8 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
3261
3567
  );
3262
3568
  }
3263
3569
  group.open = beginElementGroup(ctx, opacity, style.filter);
3264
- if (style.boxShadow) {
3265
- drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
3266
- }
3570
+ const boxShadows = style.boxShadow ? parseBoxShadow(style.boxShadow, style.color ?? "black") : [];
3571
+ drawBoxShadow(ctx, x, y, width, height, boxShadows, borderRadius);
3267
3572
  const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
3268
3573
  if (isClipped) {
3269
3574
  applyClip(ctx, x, y, width, height, borderRadius);
@@ -3298,15 +3603,14 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
3298
3603
  } else {
3299
3604
  const urlMatch = layer.match(/url\(["']?(.*?)["']?\)/);
3300
3605
  if (urlMatch) {
3301
- if (hasRadius(borderRadius)) {
3302
- applyClip(ctx, x, y, width, height, borderRadius);
3303
- }
3304
3606
  const image = await cachedLoadImage(
3305
3607
  renderContext.imageCache,
3306
3608
  urlMatch[1],
3307
3609
  renderContext.userAgent
3308
3610
  );
3309
3611
  const bgSize = style.backgroundSize;
3612
+ ctx.save();
3613
+ applyClip(ctx, x, y, width, height, borderRadius);
3310
3614
  if (bgSize === "cover") {
3311
3615
  const r = computeCover(
3312
3616
  image.width,
@@ -3347,19 +3651,24 @@ async function paintNode(ctx, node, x, y, opacity, renderContext, emojiStyle, gr
3347
3651
  tileW = image.width;
3348
3652
  tileH = image.height;
3349
3653
  }
3654
+ const drawable = tileW > 0 && tileH > 0 && Number.isFinite(tileW) && Number.isFinite(tileH);
3350
3655
  const repeat = style.backgroundRepeat ?? "repeat";
3351
3656
  const stepX = repeat === "repeat" || repeat === "repeat-x" ? tileW : width;
3352
3657
  const stepY = repeat === "repeat" || repeat === "repeat-y" ? tileH : height;
3353
- for (let ty = y; ty < y + height; ty += stepY) {
3354
- for (let tx = x; tx < x + width; tx += stepX) {
3355
- ctx.drawImage(image, tx, ty, tileW, tileH);
3658
+ if (drawable) {
3659
+ for (let ty = y; ty < y + height; ty += stepY) {
3660
+ for (let tx = x; tx < x + width; tx += stepX) {
3661
+ ctx.drawImage(image, tx, ty, tileW, tileH);
3662
+ }
3356
3663
  }
3357
3664
  }
3358
3665
  }
3666
+ ctx.restore();
3359
3667
  }
3360
3668
  }
3361
3669
  }
3362
3670
  }
3671
+ drawInsetBoxShadow(ctx, x, y, width, height, boxShadows, borderRadius, style);
3363
3672
  if (renderContext.debug) {
3364
3673
  ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
3365
3674
  ctx.lineWidth = 1;
@@ -3833,6 +4142,7 @@ import Yoga, {
3833
4142
  MeasureMode,
3834
4143
  Overflow,
3835
4144
  PositionType,
4145
+ Unit,
3836
4146
  Wrap
3837
4147
  } from "yoga-layout";
3838
4148
  function createYogaNode() {
@@ -3851,6 +4161,10 @@ function freeYogaNode(node) {
3851
4161
  }
3852
4162
 
3853
4163
  // src/jsx/style/properties.ts
4164
+ function toLayoutUnit(px) {
4165
+ if (!Number.isFinite(px)) return px;
4166
+ return Math.trunc(Math.round(px * 64 * 1e6) / 1e6) / 64;
4167
+ }
3854
4168
  function applyStylesToYoga(node, style) {
3855
4169
  if (style.display === "none") {
3856
4170
  node.setDisplay(Display.None);
@@ -3928,14 +4242,14 @@ function applyStylesToYoga(node, style) {
3928
4242
  node.setFlexShrink(style.flexShrink ?? 0);
3929
4243
  if (style.flexBasis !== void 0) {
3930
4244
  if (typeof style.flexBasis === "number") {
3931
- node.setFlexBasis(style.flexBasis);
4245
+ node.setFlexBasis(toLayoutUnit(style.flexBasis));
3932
4246
  } else if (String(style.flexBasis).endsWith("%")) {
3933
4247
  node.setFlexBasis(String(style.flexBasis));
3934
4248
  } else if (style.flexBasis === "auto") {
3935
4249
  node.setFlexBasis("auto");
3936
4250
  } else {
3937
4251
  const n = parseFloat(String(style.flexBasis));
3938
- if (!isNaN(n)) node.setFlexBasis(n);
4252
+ if (!isNaN(n)) node.setFlexBasis(toLayoutUnit(n));
3939
4253
  }
3940
4254
  }
3941
4255
  applyDimension(node, "setWidth", style.width);
@@ -3962,16 +4276,20 @@ function applyStylesToYoga(node, style) {
3962
4276
  applyEdgeValue(node, "setPadding", Edge.Bottom, style.paddingBottom);
3963
4277
  applyEdgeValue(node, "setPadding", Edge.Left, style.paddingLeft);
3964
4278
  if (style.borderTopWidth !== void 0)
3965
- node.setBorder(Edge.Top, style.borderTopWidth);
4279
+ node.setBorder(Edge.Top, toLayoutUnit(style.borderTopWidth));
3966
4280
  if (style.borderRightWidth !== void 0)
3967
- node.setBorder(Edge.Right, style.borderRightWidth);
4281
+ node.setBorder(Edge.Right, toLayoutUnit(style.borderRightWidth));
3968
4282
  if (style.borderBottomWidth !== void 0)
3969
- node.setBorder(Edge.Bottom, style.borderBottomWidth);
4283
+ node.setBorder(
4284
+ Edge.Bottom,
4285
+ toLayoutUnit(style.borderBottomWidth)
4286
+ );
3970
4287
  if (style.borderLeftWidth !== void 0)
3971
- node.setBorder(Edge.Left, style.borderLeftWidth);
3972
- if (style.rowGap !== void 0) node.setGap(Gutter.Row, style.rowGap);
4288
+ node.setBorder(Edge.Left, toLayoutUnit(style.borderLeftWidth));
4289
+ if (style.rowGap !== void 0)
4290
+ node.setGap(Gutter.Row, toLayoutUnit(style.rowGap));
3973
4291
  if (style.columnGap !== void 0)
3974
- node.setGap(Gutter.Column, style.columnGap);
4292
+ node.setGap(Gutter.Column, toLayoutUnit(style.columnGap));
3975
4293
  if (style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden") {
3976
4294
  node.setOverflow(Overflow.Hidden);
3977
4295
  } else {
@@ -3987,7 +4305,7 @@ function applyDimension(node, setter, value) {
3987
4305
  return;
3988
4306
  }
3989
4307
  if (typeof value === "number") {
3990
- node[setter](value);
4308
+ node[setter](toLayoutUnit(value));
3991
4309
  return;
3992
4310
  }
3993
4311
  const s = String(value);
@@ -3995,7 +4313,7 @@ function applyDimension(node, setter, value) {
3995
4313
  node[setter](s);
3996
4314
  } else {
3997
4315
  const n = parseFloat(s);
3998
- if (!isNaN(n)) node[setter](n);
4316
+ if (!isNaN(n)) node[setter](toLayoutUnit(n));
3999
4317
  }
4000
4318
  }
4001
4319
  function applyEdgeValue(node, setter, edge, value) {
@@ -4005,7 +4323,7 @@ function applyEdgeValue(node, setter, edge, value) {
4005
4323
  return;
4006
4324
  }
4007
4325
  if (typeof value === "number") {
4008
- node[setter](edge, value);
4326
+ node[setter](edge, toLayoutUnit(value));
4009
4327
  return;
4010
4328
  }
4011
4329
  const s = String(value);
@@ -4013,7 +4331,7 @@ function applyEdgeValue(node, setter, edge, value) {
4013
4331
  node[setter](edge, s);
4014
4332
  } else {
4015
4333
  const n = parseFloat(s);
4016
- if (!isNaN(n)) node[setter](edge, n);
4334
+ if (!isNaN(n)) node[setter](edge, toLayoutUnit(n));
4017
4335
  }
4018
4336
  }
4019
4337
 
@@ -4041,12 +4359,46 @@ async function buildLayoutTree(element, containerWidth, containerHeight, emojiEn
4041
4359
  rootYogaNode.setHeight(containerHeight);
4042
4360
  rootYogaNode.setFlexDirection(FlexDirection.Row);
4043
4361
  rootYogaNode.insertChild(elementYogaNode, 0);
4044
- rootYogaNode.calculateLayout(containerWidth, containerHeight);
4045
- settleText(
4046
- elementNode,
4047
- () => rootYogaNode.calculateLayout(containerWidth, containerHeight),
4048
- renderContext.debug
4049
- );
4362
+ let layouts = 0;
4363
+ const compute = () => {
4364
+ layouts++;
4365
+ rootYogaNode.calculateLayout(containerWidth, containerHeight);
4366
+ };
4367
+ const autoMinimums = collectAutoMinimums(elementNode);
4368
+ const layOut = () => {
4369
+ compute();
4370
+ for (const node of autoMinimums) node.autoMinimum.refreshed = false;
4371
+ for (let pass = 0; pass < MAX_AUTO_MINIMUM_RELAYOUTS && enforceAutoMinimums(autoMinimums); pass++) {
4372
+ compute();
4373
+ }
4374
+ };
4375
+ layOut();
4376
+ settleText(elementNode, layOut, renderContext.debug);
4377
+ const limit = RUNAWAY_SIZE * Math.max(containerWidth, containerHeight);
4378
+ for (let pass = 0; pass < MAX_RUNAWAY_RELAYOUTS; pass++) {
4379
+ const runaway2 = findRunawayItems(elementNode, limit);
4380
+ if (runaway2.length === 0) break;
4381
+ for (const item of runaway2) pinAtMinimum(item);
4382
+ layOut();
4383
+ settleText(elementNode, layOut, renderContext.debug);
4384
+ }
4385
+ const runaway = findRunawayItems(elementNode, limit);
4386
+ if (runaway.length > 0) {
4387
+ const lines = new Set(runaway.map((item) => item.parent));
4388
+ const lifted = [...lines].flatMap((line) => line.children).filter((node) => node.autoMinimum && liftAutoMinimum(node));
4389
+ if (lifted.length > 0) {
4390
+ layOut();
4391
+ settleText(elementNode, layOut, renderContext.debug);
4392
+ }
4393
+ if (renderContext.debug) {
4394
+ console.warn(
4395
+ "[@effing/canvas] Yoga sized flex items out of all proportion" + (lifted.length > 0 ? ": laid their text out without minimums" : "")
4396
+ );
4397
+ }
4398
+ }
4399
+ if (renderContext.debug && layouts > 5) {
4400
+ console.warn(`[@effing/canvas] computed the layout ${layouts} times`);
4401
+ }
4050
4402
  const elementLayout = extractLayout(elementNode, elementYogaNode);
4051
4403
  freeYogaNode(rootYogaNode);
4052
4404
  const tree = {
@@ -4071,20 +4423,9 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
4071
4423
  yogaNode
4072
4424
  };
4073
4425
  }
4074
- if (typeof element === "string" || typeof element === "number") {
4075
- const text = String(element);
4076
- const style2 = resolveStyle(void 0, parentStyle);
4077
- const textMeasure = new TextMeasure(text, style2, emojiEnabled);
4078
- setTextMeasure(yogaNode, textMeasure);
4079
- return {
4080
- type: "text",
4081
- style: style2,
4082
- children: [],
4083
- textContent: text,
4084
- textMeasure,
4085
- props: {},
4086
- yogaNode
4087
- };
4426
+ if (isText(element) || isTextRun(element)) {
4427
+ const text = isTextRun(element) ? textContentOf(element) : String(element);
4428
+ return buildTextNode(text, parentStyle, yogaNode, emojiEnabled);
4088
4429
  }
4089
4430
  const el = element;
4090
4431
  const type = el.type;
@@ -4181,14 +4522,18 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
4181
4522
  yogaNode
4182
4523
  };
4183
4524
  }
4184
- const items = flattenChildren(props.children);
4185
- const onlyText = items.length === 1 && isText(items[0]) ? items[0] : null;
4186
- if (onlyText !== null && !isWhiteSpaceOnly(String(onlyText))) {
4187
- const textContent = String(onlyText);
4188
- const childStyle = resolveStyle(void 0, style);
4525
+ const items = flattenChildren(props.children).filter(
4526
+ (item) => !isTextRun(item) || !isBlankRun(item)
4527
+ );
4528
+ const onlyText = items.length === 1 && isTextRun(items[0]) ? items[0] : null;
4529
+ if (onlyText !== null) {
4189
4530
  const childYogaNode = createTextYogaNode();
4190
- const textMeasure = new TextMeasure(textContent, childStyle, emojiEnabled);
4191
- setTextMeasure(childYogaNode, textMeasure);
4531
+ const child = buildTextNode(
4532
+ textContentOf(onlyText),
4533
+ style,
4534
+ childYogaNode,
4535
+ emojiEnabled
4536
+ );
4192
4537
  const jc = style.justifyContent;
4193
4538
  if (!jc || jc === "flex-start") {
4194
4539
  childYogaNode.setFlexGrow(1);
@@ -4198,28 +4543,29 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
4198
4543
  if (style.flexShrink === void 0) {
4199
4544
  yogaNode.setFlexShrink(1);
4200
4545
  }
4546
+ const textMeasure = child.textMeasure;
4547
+ child.autoMinimum = {
4548
+ textMeasure,
4549
+ column: !isRow(style),
4550
+ text: childYogaNode
4551
+ };
4201
4552
  return {
4202
4553
  type: tagName,
4203
4554
  style,
4204
- children: [
4205
- {
4206
- type: "text",
4207
- style: childStyle,
4208
- children: [],
4209
- textContent,
4210
- textMeasure,
4211
- props: {},
4212
- yogaNode: childYogaNode
4213
- }
4214
- ],
4555
+ children: [child],
4215
4556
  props,
4216
- yogaNode
4557
+ yogaNode,
4558
+ autoMinimum: hasAutoMinimum(style, parentStyle) ? {
4559
+ textMeasure,
4560
+ style,
4561
+ column: !isRow(parentStyle),
4562
+ text: childYogaNode
4563
+ } : void 0
4217
4564
  };
4218
4565
  }
4219
4566
  const children = [];
4220
4567
  for (const rendered of items) {
4221
- if (typeof rendered === "string" && isWhiteSpaceOnly(rendered)) continue;
4222
- const childYogaNode = isText(rendered) ? createTextYogaNode() : createYogaNode();
4568
+ const childYogaNode = isTextRun(rendered) ? createTextYogaNode() : createYogaNode();
4223
4569
  yogaNode.insertChild(childYogaNode, children.length);
4224
4570
  children.push(
4225
4571
  await buildNode(
@@ -4275,11 +4621,248 @@ function settleText(root, relayout, debug) {
4275
4621
  );
4276
4622
  }
4277
4623
  }
4624
+ function buildTextNode(text, parentStyle, yogaNode, emojiEnabled) {
4625
+ const style = resolveStyle(void 0, parentStyle);
4626
+ const textMeasure = new TextMeasure(text, style, emojiEnabled);
4627
+ setTextMeasure(yogaNode, textMeasure);
4628
+ return {
4629
+ type: "text",
4630
+ style,
4631
+ children: [],
4632
+ textContent: text,
4633
+ textMeasure,
4634
+ props: {},
4635
+ yogaNode
4636
+ };
4637
+ }
4638
+ var isRow = (style) => style.flexDirection === void 0 || style.flexDirection === "row" || style.flexDirection === "row-reverse";
4639
+ function hasAutoMinimum(style, parentStyle) {
4640
+ const min = isRow(parentStyle) ? style.minWidth : style.minHeight;
4641
+ return style.position !== "absolute" && (min === void 0 || min === "auto") && ![style.overflow, style.overflowX, style.overflowY].some(
4642
+ (overflow) => overflow === "hidden" || overflow === "scroll" || overflow === "auto"
4643
+ );
4644
+ }
4645
+ var MAX_AUTO_MINIMUM_RELAYOUTS = 4;
4646
+ function collectAutoMinimums(root) {
4647
+ const nodes = [];
4648
+ const collect = (node) => {
4649
+ if (node.style.display === "none") return;
4650
+ if (node.autoMinimum) nodes.push(node);
4651
+ node.children.forEach(collect);
4652
+ };
4653
+ collect(root);
4654
+ return nodes;
4655
+ }
4656
+ function enforceAutoMinimums(nodes) {
4657
+ let enforced = false;
4658
+ for (const node of nodes) {
4659
+ const autoMinimum = node.autoMinimum;
4660
+ if (autoMinimum.lifted) continue;
4661
+ if (autoMinimum.column) {
4662
+ if (enforceAutoMinimumHeight(node)) enforced = true;
4663
+ continue;
4664
+ }
4665
+ if (autoMinimum.lift) continue;
4666
+ const { yogaNode } = node;
4667
+ const { textMeasure, style } = autoMinimum;
4668
+ const width = yogaNode.getComputedWidth();
4669
+ const tolerance = style ? 1 : 1e-3;
4670
+ const edges = style ? horizontalEdges(yogaNode, style) : 0;
4671
+ if (width + tolerance >= textMeasure.minContentBound + edges) continue;
4672
+ const minimum = autoMinimumWidth(node);
4673
+ if (minimum === void 0 || width + tolerance >= minimum) continue;
4674
+ autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
4675
+ atBasis: textMeasure.unbreakable && (!style || isAuto(style.width) && isAuto(style.flexBasis)),
4676
+ // The text in an element sizes the element's own flex basis, which a
4677
+ // pin would take from it.
4678
+ pin: style !== void 0,
4679
+ column: false
4680
+ });
4681
+ enforced = true;
4682
+ }
4683
+ return enforced;
4684
+ }
4685
+ function enforceAutoMinimumHeight(node) {
4686
+ const autoMinimum = node.autoMinimum;
4687
+ const { yogaNode } = node;
4688
+ const { style } = autoMinimum;
4689
+ const minimum = autoMinimumHeight(node);
4690
+ if (minimum === void 0) return false;
4691
+ const height = yogaNode.getComputedHeight();
4692
+ const tolerance = style ? 0.5 : 1e-3;
4693
+ const short = height + tolerance < minimum;
4694
+ const atBasis = !style || isAuto(style.height) && isAuto(style.flexBasis);
4695
+ if (autoMinimum.lift) {
4696
+ const { held } = autoMinimum;
4697
+ if (held === void 0) {
4698
+ if (!short) return false;
4699
+ } else if (Math.abs(held - minimum) < tolerance || // In a column that wraps, the heights decide the widths: an item is
4700
+ // only ever held higher there, or they could swing back and forth.
4701
+ minimum < held && yogaNode.getParent()?.getFlexWrap() !== Wrap.NoWrap) {
4702
+ if (height + (style ? 1 : 1e-3) >= held || autoMinimum.refreshed) {
4703
+ return false;
4704
+ }
4705
+ autoMinimum.text.markDirty();
4706
+ autoMinimum.refreshed = true;
4707
+ return true;
4708
+ }
4709
+ autoMinimum.lift();
4710
+ } else if (!short) {
4711
+ return false;
4712
+ } else if (atBasis) {
4713
+ autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
4714
+ atBasis: true,
4715
+ pin: false,
4716
+ column: true
4717
+ });
4718
+ return true;
4719
+ }
4720
+ autoMinimum.held = minimum;
4721
+ autoMinimum.lift = holdAtMinimum(yogaNode, minimum, {
4722
+ atBasis: false,
4723
+ pin: !atBasis,
4724
+ column: true
4725
+ });
4726
+ return true;
4727
+ }
4728
+ var isAuto = (value) => value === void 0 || value === "auto";
4729
+ function horizontalEdges(yogaNode, style) {
4730
+ const lengths = style && [
4731
+ style.paddingLeft,
4732
+ style.paddingRight,
4733
+ style.borderLeftWidth,
4734
+ style.borderRightWidth
4735
+ ];
4736
+ if (lengths?.every((v) => v === void 0 || typeof v === "number")) {
4737
+ return lengths.reduce(
4738
+ (sum, v) => sum + toLayoutUnit(v ?? 0),
4739
+ 0
4740
+ );
4741
+ }
4742
+ return yogaNode.getComputedPadding(Edge.Left) + yogaNode.getComputedPadding(Edge.Right) + yogaNode.getComputedBorder(Edge.Left) + yogaNode.getComputedBorder(Edge.Right);
4743
+ }
4744
+ function autoMinimumWidth(node) {
4745
+ const { yogaNode } = node;
4746
+ const { textMeasure, style } = node.autoMinimum;
4747
+ if (!style) return textMeasure.minContentWidth;
4748
+ const parent = yogaNode.getParent();
4749
+ return capped(
4750
+ textMeasure.minContentWidth + horizontalEdges(yogaNode, style),
4751
+ [style.width, style.maxWidth],
4752
+ parent ? parent.getComputedWidth() - horizontalEdges(parent) : NaN
4753
+ );
4754
+ }
4755
+ function autoMinimumHeight(node) {
4756
+ const { yogaNode } = node;
4757
+ const { textMeasure, style, text } = node.autoMinimum;
4758
+ const height = textMeasure.heightAt(text.getComputedWidth());
4759
+ if (!style) return height;
4760
+ const parent = yogaNode.getParent();
4761
+ return capped(
4762
+ height + verticalEdges(yogaNode),
4763
+ [style.height, style.maxHeight],
4764
+ parent ? parent.getComputedHeight() - verticalEdges(parent) : NaN
4765
+ );
4766
+ }
4767
+ function capped(minimum, sizes, parentContentSize) {
4768
+ for (const value of sizes) {
4769
+ if (isAuto(value)) continue;
4770
+ let cap;
4771
+ if (typeof value === "number") {
4772
+ cap = toLayoutUnit(value);
4773
+ } else if (value.endsWith("%") && Number.isFinite(parentContentSize)) {
4774
+ cap = parseFloat(value) / 100 * parentContentSize;
4775
+ } else {
4776
+ return void 0;
4777
+ }
4778
+ if (!Number.isFinite(cap)) return void 0;
4779
+ minimum = Math.min(minimum, cap);
4780
+ }
4781
+ return minimum;
4782
+ }
4783
+ var verticalEdges = (yogaNode) => yogaNode.getComputedPadding(Edge.Top) + yogaNode.getComputedPadding(Edge.Bottom) + yogaNode.getComputedBorder(Edge.Top) + yogaNode.getComputedBorder(Edge.Bottom);
4784
+ function holdAtMinimum(yogaNode, minimum, { atBasis, pin, column }) {
4785
+ const flexShrink = yogaNode.getFlexShrink();
4786
+ if (atBasis) {
4787
+ yogaNode.setFlexShrink(0);
4788
+ return () => yogaNode.setFlexShrink(flexShrink);
4789
+ }
4790
+ const wraps = yogaNode.getParent()?.getFlexWrap() !== Wrap.NoWrap;
4791
+ if (!pin || wraps) {
4792
+ const setMin = (value) => column ? yogaNode.setMinHeight(value) : yogaNode.setMinWidth(value);
4793
+ setMin(minimum);
4794
+ return () => setMin(void 0);
4795
+ }
4796
+ const flexGrow = yogaNode.getFlexGrow();
4797
+ const flexBasis = yogaNode.getFlexBasis();
4798
+ yogaNode.setFlexBasis(minimum);
4799
+ yogaNode.setFlexShrink(0);
4800
+ yogaNode.setFlexGrow(0);
4801
+ return () => {
4802
+ yogaNode.setFlexShrink(flexShrink);
4803
+ yogaNode.setFlexGrow(flexGrow);
4804
+ if (flexBasis.unit === Unit.Point) yogaNode.setFlexBasis(flexBasis.value);
4805
+ else if (flexBasis.unit === Unit.Percent)
4806
+ yogaNode.setFlexBasis(`${flexBasis.value}%`);
4807
+ else yogaNode.setFlexBasis("auto");
4808
+ };
4809
+ }
4810
+ function liftAutoMinimum(node) {
4811
+ const autoMinimum = node.autoMinimum;
4812
+ const { lift } = autoMinimum;
4813
+ lift?.();
4814
+ autoMinimum.lift = void 0;
4815
+ autoMinimum.lifted = true;
4816
+ return lift !== void 0;
4817
+ }
4818
+ var RUNAWAY_SIZE = 100;
4819
+ var MAX_RUNAWAY_RELAYOUTS = 2;
4820
+ var isRunaway = (size, limit) => !(Math.abs(size) <= limit);
4821
+ function findRunawayItems(root, limit) {
4822
+ const items = [];
4823
+ const visit = (node, parent) => {
4824
+ if (node.style.display === "none") return;
4825
+ const { yogaNode } = node;
4826
+ if (parent) {
4827
+ const row = [FlexDirection.Row, FlexDirection.RowReverse].includes(
4828
+ parent.yogaNode.getFlexDirection()
4829
+ );
4830
+ const size = (n) => row ? n.getComputedWidth() : n.getComputedHeight();
4831
+ if (yogaNode.getFlexShrink() > 0 && isRunaway(size(yogaNode), limit) && !isRunaway(size(parent.yogaNode), limit)) {
4832
+ items.push({ node, parent, row });
4833
+ return;
4834
+ }
4835
+ }
4836
+ for (const child of node.children) visit(child, node);
4837
+ };
4838
+ visit(root);
4839
+ return items;
4840
+ }
4841
+ function pinAtMinimum({ node, parent, row }) {
4842
+ const { yogaNode } = node;
4843
+ const autoMinimum = node.autoMinimum;
4844
+ const auto = autoMinimum && autoMinimum.column !== row ? row ? autoMinimumWidth(node) : autoMinimumHeight(node) : 0;
4845
+ yogaNode.setFlexBasis(
4846
+ Math.max(minimumSize(yogaNode, parent.yogaNode, row), auto ?? 0)
4847
+ );
4848
+ yogaNode.setFlexShrink(0);
4849
+ }
4850
+ function minimumSize(yogaNode, parent, row) {
4851
+ const [start, end] = row ? [Edge.Left, Edge.Right] : [Edge.Top, Edge.Bottom];
4852
+ const edges = (node) => node.getComputedPadding(start) + node.getComputedPadding(end) + node.getComputedBorder(start) + node.getComputedBorder(end);
4853
+ const min = row ? yogaNode.getMinWidth() : yogaNode.getMinHeight();
4854
+ let minimum = 0;
4855
+ if (min.unit === Unit.Point) {
4856
+ minimum = min.value;
4857
+ } else if (min.unit === Unit.Percent) {
4858
+ const parentSize = row ? parent.getComputedWidth() : parent.getComputedHeight();
4859
+ minimum = min.value / 100 * (parentSize - edges(parent));
4860
+ }
4861
+ return Math.max(minimum, edges(yogaNode));
4862
+ }
4278
4863
  function setTextMeasure(yogaNode, textMeasure) {
4279
4864
  yogaNode.setMeasureFunc(
4280
- (width, widthMode) => textMeasure.measure(
4281
- widthMode === MeasureMode.Undefined || Number.isNaN(width) ? Infinity : width
4282
- )
4865
+ (width, widthMode) => widthMode === MeasureMode.Undefined || Number.isNaN(width) ? textMeasure.measure(Infinity) : textMeasure.measure(width, widthMode === MeasureMode.Exactly)
4283
4866
  );
4284
4867
  }
4285
4868
  var isText = (node) => typeof node === "string" || typeof node === "number";
@@ -4292,6 +4875,15 @@ function renderComponents(node) {
4292
4875
  }
4293
4876
  return node;
4294
4877
  }
4878
+ var TextRun = class {
4879
+ pieces;
4880
+ constructor(...pieces) {
4881
+ this.pieces = pieces;
4882
+ }
4883
+ };
4884
+ var isTextRun = (node) => node instanceof TextRun;
4885
+ var textContentOf = (run) => run.pieces.length === 1 ? run.pieces[0] : run.pieces;
4886
+ var isBlankRun = (run) => run.pieces.length === 1 && isWhiteSpaceOnly(run.pieces[0]);
4295
4887
  function flattenChildren(node, items = []) {
4296
4888
  const rendered = renderComponents(node);
4297
4889
  if (rendered === null || rendered === void 0 || typeof rendered === "boolean") {
@@ -4306,13 +4898,22 @@ function flattenChildren(node, items = []) {
4306
4898
  return flattenChildren(el.props?.children, items);
4307
4899
  }
4308
4900
  const last = items[items.length - 1];
4309
- if (isText(rendered) && items.length > 0 && isText(last)) {
4310
- items[items.length - 1] = `${last}${rendered}`;
4311
- } else {
4901
+ if (isText(rendered)) {
4902
+ if (isTextRun(last)) {
4903
+ last.pieces[last.pieces.length - 1] += String(rendered);
4904
+ } else {
4905
+ items.push(new TextRun(String(rendered)));
4906
+ }
4907
+ } else if (!isBreak(rendered)) {
4312
4908
  items.push(rendered);
4909
+ } else if (!isHidden(rendered)) {
4910
+ if (isTextRun(last)) last.pieces.push("");
4911
+ else items.push(new TextRun("", ""));
4313
4912
  }
4314
4913
  return items;
4315
4914
  }
4915
+ var isBreak = (node) => typeof node === "object" && node !== null && node.type === "br";
4916
+ var isHidden = (el) => el.props.style?.display === "none";
4316
4917
  function floorToPixel(value) {
4317
4918
  const rounded = Math.round(value);
4318
4919
  return Math.abs(value - rounded) < 1e-4 ? rounded : Math.floor(value);