@inditextech/weave-sdk 5.7.0-SNAPSHOT.597.1 → 5.7.0-SNAPSHOT.600.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/dist/sdk.node.js CHANGED
@@ -4774,12 +4774,39 @@ function registerKeyboardHandlers(ctx) {
4774
4774
  }
4775
4775
  }
4776
4776
  if (e.code === "Backspace" || e.code === "Delete") {
4777
- Promise.resolve().then(() => {
4777
+ queueMicrotask(() => {
4778
4778
  ctx.removeSelectedNodes();
4779
4779
  });
4780
4780
  return;
4781
4781
  }
4782
4782
  }, { signal });
4783
+ const isModifier = (e) => e.key === "Control" || e.key === "Meta";
4784
+ const hideHover = () => ctx.getTransformerController().getHoverTransformer().nodes([]);
4785
+ const restoreHover = () => {
4786
+ const pos = stage.getPointerPosition();
4787
+ const shape = pos ? stage.getIntersection(pos) : null;
4788
+ if (!shape) return;
4789
+ shape.fire("pointerover", {
4790
+ evt: {
4791
+ ctrlKey: false,
4792
+ metaKey: false
4793
+ },
4794
+ target: shape
4795
+ }, true);
4796
+ };
4797
+ window.addEventListener("keydown", (e) => {
4798
+ if (isModifier(e)) hideHover();
4799
+ }, {
4800
+ signal,
4801
+ capture: true
4802
+ });
4803
+ window.addEventListener("keyup", (e) => {
4804
+ if (isModifier(e)) restoreHover();
4805
+ }, {
4806
+ signal,
4807
+ capture: true
4808
+ });
4809
+ window.addEventListener("blur", restoreHover, { signal });
4783
4810
  stage.container().addEventListener("keyup", (e) => {
4784
4811
  if (e.code === "Space") ctx.setSpaceKeyPressed(false);
4785
4812
  }, { signal });
@@ -6535,7 +6562,10 @@ var WeaveNode = class {
6535
6562
  }
6536
6563
  handleMouseOver(e, node) {
6537
6564
  const stage = this.instance.getStage();
6538
- if (e.evt.ctrlKey || e.evt.metaKey) return false;
6565
+ if (e.evt.ctrlKey || e.evt.metaKey) {
6566
+ this.hideHoverState();
6567
+ return false;
6568
+ }
6539
6569
  const user = this.instance.getStore().getUser();
6540
6570
  const activeAction = this.instance.getActiveAction();
6541
6571
  const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled();
@@ -6582,7 +6612,10 @@ var WeaveNode = class {
6582
6612
  return cancelBubble;
6583
6613
  }
6584
6614
  handleMouseout(e, node) {
6585
- if (e.evt?.ctrlKey || e.evt?.metaKey) return false;
6615
+ if (e.evt?.ctrlKey || e.evt?.metaKey) {
6616
+ this.hideHoverState();
6617
+ return false;
6618
+ }
6586
6619
  if (this.instance.getInstanceRecursive(node)) this.hideHoverState();
6587
6620
  }
6588
6621
  create(key, props) {
@@ -9505,7 +9538,7 @@ var WeaveRegisterManager = class {
9505
9538
  };
9506
9539
  //#endregion
9507
9540
  //#region package.json
9508
- var version = "5.7.0-SNAPSHOT.597.1";
9541
+ var version = "5.7.0-SNAPSHOT.600.1";
9509
9542
  //#endregion
9510
9543
  //#region src/managers/setup.ts
9511
9544
  var WeaveSetupManager = class {
@@ -13764,7 +13797,7 @@ var WeaveTextNode = class extends WeaveNode {
13764
13797
  delete cleanedAttrs.dragBoundFunc;
13765
13798
  if (cleanedAttrs.link) {
13766
13799
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
13767
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
13800
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
13768
13801
  }
13769
13802
  return {
13770
13803
  key: attrs.id ?? "",
@@ -14400,6 +14433,31 @@ var WeaveTextNode = class extends WeaveNode {
14400
14433
  };
14401
14434
  //#endregion
14402
14435
  //#region src/nodes/text-v2/model.ts
14436
+ const MIN_LINE_HEIGHT = .5;
14437
+ /** Valid `lineHeight` mark of a run, or undefined (use the node value). */
14438
+ function runLineHeight(m) {
14439
+ const v = m?.lineHeight;
14440
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(4, Math.max(MIN_LINE_HEIGHT, v)) : void 0;
14441
+ }
14442
+ /** Only absolute http(s) URLs are links (same rule as the legacy node). */
14443
+ function isSafeLink(value) {
14444
+ if (typeof value !== "string") return false;
14445
+ try {
14446
+ return ["http:", "https:"].includes(new URL(value).protocol);
14447
+ } catch {
14448
+ return false;
14449
+ }
14450
+ }
14451
+ /** Block fields that describe structure (not content). */
14452
+ function blockFields(block, withStart) {
14453
+ const out = {};
14454
+ if (!block) return out;
14455
+ if (block.kind) out.kind = block.kind;
14456
+ if (block.depth) out.depth = block.depth;
14457
+ if (block.align) out.align = block.align;
14458
+ if (withStart && block.start !== void 0) out.start = block.start;
14459
+ return out;
14460
+ }
14403
14461
  function isEmptyMarks(m) {
14404
14462
  return !m || Object.keys(m).length === 0;
14405
14463
  }
@@ -14480,7 +14538,7 @@ function marksAt(runs, offset) {
14480
14538
  * blocks) with plain `insert`. Marks on untouched text are preserved; the
14481
14539
  * inserted text inherits marks of the character before `start`.
14482
14540
  */
14483
- function replaceRange(blocks, start, end, insert) {
14541
+ function replaceRange(blocks, start, end, insert, insertMarks) {
14484
14542
  const total = blocksToText(blocks).length;
14485
14543
  start = Math.max(0, Math.min(start, total));
14486
14544
  end = Math.max(start, Math.min(end, total));
@@ -14489,6 +14547,7 @@ function replaceRange(blocks, start, end, insert) {
14489
14547
  const prefixBlocks = [];
14490
14548
  const suffixBlocks = [];
14491
14549
  let inheritedMarks;
14550
+ let startBlock;
14492
14551
  let pos = 0;
14493
14552
  blocks.forEach((block) => {
14494
14553
  const len = blockText(block).length;
@@ -14501,11 +14560,13 @@ function replaceRange(blocks, start, end, insert) {
14501
14560
  if (start >= bStart && start <= bEnd) {
14502
14561
  const local = start - bStart;
14503
14562
  before = sliceRuns(block.runs, 0, local);
14563
+ startBlock = block;
14504
14564
  inheritedMarks = marksAt(block.runs, local);
14505
14565
  }
14506
14566
  if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
14507
14567
  }
14508
14568
  });
14569
+ if (insertMarks !== void 0) inheritedMarks = insertMarks ?? void 0;
14509
14570
  const lines = insert.split("\n");
14510
14571
  const mk = (t) => t === "" ? [] : [{
14511
14572
  t,
@@ -14516,7 +14577,10 @@ function replaceRange(blocks, start, end, insert) {
14516
14577
  let runs = mk(line);
14517
14578
  if (idx === 0) runs = [...before, ...runs];
14518
14579
  if (idx === lines.length - 1) runs = [...runs, ...after];
14519
- middle.push({ runs: normalizeRuns(runs) });
14580
+ middle.push({
14581
+ ...blockFields(startBlock, idx === 0),
14582
+ runs: normalizeRuns(runs)
14583
+ });
14520
14584
  });
14521
14585
  return [
14522
14586
  ...prefixBlocks,
@@ -14524,8 +14588,331 @@ function replaceRange(blocks, start, end, insert) {
14524
14588
  ...suffixBlocks
14525
14589
  ];
14526
14590
  }
14591
+ function safeBase(base) {
14592
+ return Number.isFinite(base) && base > 0 ? base : 1;
14593
+ }
14594
+ /** px -> stored ratio (4 decimals). */
14595
+ function sizeToRatio(px, base) {
14596
+ return Math.round(px / safeBase(base) * 1e4) / 1e4;
14597
+ }
14598
+ /** stored ratio -> px (rounded to 0.1). Invalid ratios resolve to the base. */
14599
+ function ratioToSize(ratio, base) {
14600
+ const b = safeBase(base);
14601
+ return Math.round((typeof ratio === "number" && ratio > 0 ? ratio : 1) * b * 10) / 10;
14602
+ }
14603
+ /** Effective ratio of a run (invalid or missing -> 1). */
14604
+ function runRatio(m) {
14605
+ const r = m?.size;
14606
+ return typeof r === "number" && Number.isFinite(r) && r > 0 ? r : 1;
14607
+ }
14608
+ function normalizeMarks(m) {
14609
+ const out = {};
14610
+ for (const [k, v] of Object.entries(m)) {
14611
+ if (v === void 0 || v === false || v === null || v === "") continue;
14612
+ if (k === "size" && v === 1) continue;
14613
+ out[k] = v;
14614
+ }
14615
+ return Object.keys(out).length === 0 ? void 0 : out;
14616
+ }
14617
+ /** Normalized marks of a run, or undefined when empty. */
14618
+ function cleanMarks(m) {
14619
+ return m ? normalizeMarks(m) : void 0;
14620
+ }
14621
+ function blockSpans(blocks) {
14622
+ const out = [];
14623
+ let pos = 0;
14624
+ blocks.forEach((b, index) => {
14625
+ const len = blockText(b).length;
14626
+ out.push({
14627
+ index,
14628
+ start: pos,
14629
+ end: pos + len
14630
+ });
14631
+ pos += len + 1;
14632
+ });
14633
+ return out;
14634
+ }
14635
+ /** Indices of blocks touched by [start, end] (collapsed range -> its block). */
14636
+ function blockIndicesInRange(blocks, start, end) {
14637
+ const out = [];
14638
+ for (const s of blockSpans(blocks)) if (s.end >= start && s.start <= end) {
14639
+ if (end > start && s.start === end && s.start !== start) continue;
14640
+ out.push(s.index);
14641
+ }
14642
+ return out;
14643
+ }
14644
+ /** Marks that typing at a collapsed offset would inherit. */
14645
+ function marksAtOffset(blocks, offset) {
14646
+ for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) return cleanMarks(marksAt(blocks[s.index].runs, offset - s.start));
14647
+ }
14648
+ function mapRunsInRange(block, from, to, fn) {
14649
+ const runs = [];
14650
+ let pos = 0;
14651
+ for (const run of block.runs) {
14652
+ const rs = pos;
14653
+ const re = pos + run.t.length;
14654
+ pos = re;
14655
+ const s = Math.max(from, rs);
14656
+ const e = Math.min(to, re);
14657
+ if (s >= e) {
14658
+ runs.push(run);
14659
+ continue;
14660
+ }
14661
+ if (s > rs) runs.push({
14662
+ t: run.t.slice(0, s - rs),
14663
+ m: run.m
14664
+ });
14665
+ runs.push({
14666
+ t: run.t.slice(s - rs, e - rs),
14667
+ m: fn(run.m)
14668
+ });
14669
+ if (e < re) runs.push({
14670
+ t: run.t.slice(e - rs),
14671
+ m: run.m
14672
+ });
14673
+ }
14674
+ return {
14675
+ ...block,
14676
+ runs: normalizeRuns(runs)
14677
+ };
14678
+ }
14679
+ /** Set (or remove with `undefined`) one mark over the global range. */
14680
+ function applyMark(blocks, start, end, key, value) {
14681
+ if (start >= end) return blocks;
14682
+ if (key === "lineHeight" && value !== void 0) {
14683
+ const lh = runLineHeight({ lineHeight: value });
14684
+ if (lh === void 0) return blocks;
14685
+ value = lh;
14686
+ }
14687
+ const spans = blockSpans(blocks);
14688
+ return blocks.map((block, i) => {
14689
+ const s = spans[i];
14690
+ if (s.end <= start || s.start >= end) return block;
14691
+ return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
14692
+ ...m,
14693
+ [key]: value
14694
+ }));
14695
+ });
14696
+ }
14697
+ /** Query one mark over a range; collapsed ranges report the caret marks. */
14698
+ function queryMark(blocks, start, end, key, stored) {
14699
+ const values = [];
14700
+ if (start >= end) {
14701
+ const v = (stored ?? marksAtOffset(blocks, start))?.[key];
14702
+ return v === void 0 || v === false ? { state: "none" } : {
14703
+ state: "all",
14704
+ value: v
14705
+ };
14706
+ }
14707
+ const spans = blockSpans(blocks);
14708
+ blocks.forEach((block, i) => {
14709
+ const s = spans[i];
14710
+ if (s.end <= start || s.start >= end) return;
14711
+ const from = Math.max(start, s.start) - s.start;
14712
+ const to = Math.min(end, s.end) - s.start;
14713
+ for (const r of sliceRuns(block.runs, from, to)) {
14714
+ const v = r.m?.[key];
14715
+ values.push(v === false ? void 0 : v);
14716
+ }
14717
+ });
14718
+ if (values.length === 0) return { state: "none" };
14719
+ const set = values.filter((v) => v !== void 0);
14720
+ if (set.length === 0) return { state: "none" };
14721
+ if (set.length < values.length) return { state: "some" };
14722
+ return values.every((v) => v === values[0]) ? {
14723
+ state: "all",
14724
+ value: values[0]
14725
+ } : { state: "some" };
14726
+ }
14727
+ /** Toggle a boolean mark: removes when fully set, sets otherwise. */
14728
+ function toggleMark(blocks, start, end, key) {
14729
+ return applyMark(blocks, start, end, key, queryMark(blocks, start, end, key).state === "all" ? void 0 : true);
14730
+ }
14731
+ function mapBlocks(blocks, indices, fn) {
14732
+ const set = new Set(indices);
14733
+ return blocks.map((b, i) => set.has(i) ? fn(b) : b);
14734
+ }
14735
+ function stripList(b) {
14736
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
14737
+ return rest;
14738
+ }
14739
+ /** Toggle list `kind` on the blocks of a range (all already that kind -> off). */
14740
+ function toggleList(blocks, start, end, kind) {
14741
+ const idx = blockIndicesInRange(blocks, start, end);
14742
+ const allSame = idx.every((i) => blocks[i].kind === kind);
14743
+ return mapBlocks(blocks, idx, (b) => allSame ? stripList(b) : {
14744
+ ...b,
14745
+ kind
14746
+ });
14747
+ }
14748
+ /** Set (or clear with `undefined`) the alignment of the blocks in a range. */
14749
+ function setBlockAlign(blocks, start, end, align) {
14750
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14751
+ if (align === void 0) {
14752
+ const { align: _a, ...rest } = b;
14753
+ return rest;
14754
+ }
14755
+ return {
14756
+ ...b,
14757
+ align
14758
+ };
14759
+ });
14760
+ }
14761
+ /** Increase depth of list blocks in the range (non-list blocks untouched). */
14762
+ function indentBlocks(blocks, start, end) {
14763
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => b.kind ? {
14764
+ ...b,
14765
+ depth: (b.depth ?? 0) + 1
14766
+ } : b);
14767
+ }
14768
+ /** Decrease depth; at depth 0 the list marker is removed. */
14769
+ function outdentBlocks(blocks, start, end) {
14770
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14771
+ if (!b.kind) return b;
14772
+ const d = b.depth ?? 0;
14773
+ if (d <= 0) return stripList(b);
14774
+ const next = {
14775
+ ...b,
14776
+ depth: d - 1
14777
+ };
14778
+ if (d - 1 === 0) delete next.depth;
14779
+ return next;
14780
+ });
14781
+ }
14782
+ const BULLETS = [
14783
+ "•",
14784
+ "◦",
14785
+ "▪"
14786
+ ];
14787
+ function alpha(n) {
14788
+ let s = "";
14789
+ let v = n;
14790
+ while (v > 0) {
14791
+ v--;
14792
+ s = String.fromCharCode(97 + v % 26) + s;
14793
+ v = Math.floor(v / 26);
14794
+ }
14795
+ return s;
14796
+ }
14797
+ function roman(n) {
14798
+ const map = [
14799
+ [1e3, "m"],
14800
+ [900, "cm"],
14801
+ [500, "d"],
14802
+ [400, "cd"],
14803
+ [100, "c"],
14804
+ [90, "xc"],
14805
+ [50, "l"],
14806
+ [40, "xl"],
14807
+ [10, "x"],
14808
+ [9, "ix"],
14809
+ [5, "v"],
14810
+ [4, "iv"],
14811
+ [1, "i"]
14812
+ ];
14813
+ let v = n;
14814
+ let s = "";
14815
+ for (const [k, r] of map) while (v >= k) {
14816
+ s += r;
14817
+ v -= k;
14818
+ }
14819
+ return s;
14820
+ }
14821
+ /**
14822
+ * Marker text per block (null for plain paragraphs). Ordered numbering is
14823
+ * continuous across consecutive same-depth items and restarts after a
14824
+ * non-list block, a shallower item or a kind change; `start` seeds a run.
14825
+ * Markers are layout-only: they never enter `text` or offsets.
14826
+ */
14827
+ function listMarkers(blocks) {
14828
+ const stack = [];
14829
+ return blocks.map((b) => {
14830
+ if (!b.kind) {
14831
+ stack.length = 0;
14832
+ return null;
14833
+ }
14834
+ const d = Math.max(0, Math.floor(b.depth ?? 0));
14835
+ stack.length = Math.min(stack.length, d + 1);
14836
+ while (stack.length < d) stack.push({
14837
+ kind: b.kind,
14838
+ n: 0
14839
+ });
14840
+ const cur = stack[d];
14841
+ if (!cur || cur.kind !== b.kind) stack[d] = {
14842
+ kind: b.kind,
14843
+ n: b.kind === "ol" ? Math.max(1, b.start ?? 1) : 0
14844
+ };
14845
+ else if (b.kind === "ol") cur.n += 1;
14846
+ if (b.kind === "ul") return BULLETS[d % 3];
14847
+ const n = stack[d].n;
14848
+ const style = d % 3;
14849
+ return `${style === 0 ? n : style === 1 ? alpha(n) : roman(n)}.`;
14850
+ });
14851
+ }
14852
+ /**
14853
+ * Format of the range for toolbars. A collapsed range reports the stored
14854
+ * marks (when given) or the marks typing would inherit.
14855
+ */
14856
+ function getSelectionFormat(blocks, start, end, baseFontSize, stored) {
14857
+ const q = (key) => queryMark(blocks, start, end, key, stored);
14858
+ const ratios = [];
14859
+ if (start >= end) ratios.push(runRatio(stored ?? marksAtOffset(blocks, start)));
14860
+ else {
14861
+ const spans = blockSpans(blocks);
14862
+ blocks.forEach((block, i) => {
14863
+ const s = spans[i];
14864
+ if (s.end <= start || s.start >= end) return;
14865
+ const from = Math.max(start, s.start) - s.start;
14866
+ const to = Math.min(end, s.end) - s.start;
14867
+ for (const r of sliceRuns(block.runs, from, to)) ratios.push(runRatio(r.m));
14868
+ });
14869
+ }
14870
+ let size;
14871
+ if (ratios.length === 0 || ratios.every((r) => r === ratios[0])) {
14872
+ const r = ratios[0] ?? 1;
14873
+ size = {
14874
+ state: r === 1 ? "none" : "all",
14875
+ value: ratioToSize(r, baseFontSize)
14876
+ };
14877
+ } else size = { state: "some" };
14878
+ const idx = blockIndicesInRange(blocks, start, end);
14879
+ const kinds = new Set(idx.map((i) => blocks[i].kind ?? "none"));
14880
+ let list = { kind: "none" };
14881
+ if (kinds.size === 1) {
14882
+ const kind = [...kinds][0];
14883
+ const depths = new Set(idx.map((i) => blocks[i].depth ?? 0));
14884
+ list = kind === "none" ? { kind } : {
14885
+ kind,
14886
+ ...depths.size === 1 ? { depth: [...depths][0] } : {}
14887
+ };
14888
+ } else if (kinds.size > 1) list = { kind: "mixed" };
14889
+ return {
14890
+ bold: q("bold"),
14891
+ italic: q("italic"),
14892
+ underline: q("underline"),
14893
+ strike: q("strike"),
14894
+ color: q("color"),
14895
+ family: q("family"),
14896
+ link: q("link"),
14897
+ lineHeight: q("lineHeight"),
14898
+ align: alignQuery(blocks, start, end),
14899
+ size,
14900
+ list
14901
+ };
14902
+ }
14903
+ function alignQuery(blocks, start, end) {
14904
+ const vals = blockIndicesInRange(blocks, start, end).map((i) => blocks[i].align);
14905
+ const set = vals.filter((v) => v !== void 0);
14906
+ if (set.length === 0) return { state: "none" };
14907
+ if (set.length < vals.length || set.some((v) => v !== set[0])) return { state: "some" };
14908
+ return {
14909
+ state: "all",
14910
+ value: set[0]
14911
+ };
14912
+ }
14527
14913
  //#endregion
14528
14914
  //#region src/nodes/text-v2/layout/engine.ts
14915
+ const DEFAULT_INDENT_STEP = 1.5;
14529
14916
  /** Quote multi-word families like `Konva` does so the font shorthand parses. */
14530
14917
  function normalizeFontFamily(fontFamily) {
14531
14918
  return fontFamily.split(",").map((family) => {
@@ -14536,7 +14923,51 @@ function normalizeFontFamily(fontFamily) {
14536
14923
  }).join(", ");
14537
14924
  }
14538
14925
  function fontString(s) {
14539
- return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14926
+ return `${[...`${s.fontStyle} ${s.fontVariant}`.split(/\s+/).filter((token) => token !== "" && token !== "normal"), `${s.fontSize}px`].join(" ")} ${normalizeFontFamily(s.fontFamily)}`;
14927
+ }
14928
+ /** Style of a run: node style overridden by the run marks. */
14929
+ function runStyle(style, m) {
14930
+ if (!m) return style;
14931
+ let fontStyle = style.fontStyle;
14932
+ if (m.bold || m.italic) {
14933
+ const tokens = new Set(fontStyle.split(/\s+/).filter(Boolean));
14934
+ tokens.delete("normal");
14935
+ if (m.italic) tokens.add("italic");
14936
+ if (m.bold) tokens.add("bold");
14937
+ fontStyle = tokens.size === 0 ? "normal" : Array.from(tokens).join(" ");
14938
+ }
14939
+ return {
14940
+ ...style,
14941
+ fontStyle,
14942
+ fontSize: style.fontSize * runRatio(m),
14943
+ fontFamily: typeof m.family === "string" && m.family ? m.family : style.fontFamily
14944
+ };
14945
+ }
14946
+ /** Pure fallback (same constants as Konva.Text when the browser has none). */
14947
+ const approxMetrics = (_font, size) => ({
14948
+ ascent: .91 * size,
14949
+ descent: .21 * size
14950
+ });
14951
+ const metricsCache = /* @__PURE__ */ new Map();
14952
+ /** Ascent/descent used for baseline placement (parity with Konva.Text). */
14953
+ function fontMetrics(font, size) {
14954
+ if (Konva.legacyTextRendering) return {
14955
+ ascent: 0,
14956
+ descent: 0
14957
+ };
14958
+ const hit = metricsCache.get(font);
14959
+ if (hit) return hit;
14960
+ const ctx = Konva.Util.createCanvasElement().getContext("2d");
14961
+ ctx.font = font;
14962
+ const m = ctx.measureText("M");
14963
+ const scale = size / 100;
14964
+ const out = {
14965
+ ascent: m.fontBoundingBoxAscent ?? 91 * scale,
14966
+ descent: m.fontBoundingBoxDescent ?? 21 * scale
14967
+ };
14968
+ if (metricsCache.size > 2e3) metricsCache.clear();
14969
+ metricsCache.set(font, out);
14970
+ return out;
14540
14971
  }
14541
14972
  let sharedContext = null;
14542
14973
  /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
@@ -14585,11 +15016,15 @@ function wordWrapIndex(graphemes, start, matchLen, matchWidth, maxWidth) {
14585
15016
  const seg = graphemes.slice(start, start + matchLen);
14586
15017
  return Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
14587
15018
  }
14588
- function applyAlign(lines, align, width, padding) {
14589
- if (align === "left") return;
15019
+ function applyAlign(lines, width, padding) {
14590
15020
  for (const line of lines) {
14591
- const free = width - line.width - padding * 2;
14592
- line.offsetX = align === "center" ? free / 2 : free;
15021
+ const align = line.align;
15022
+ if (align === "left") {
15023
+ line.offsetX = line.indent;
15024
+ continue;
15025
+ }
15026
+ const free = width - line.indent - line.width - padding * 2;
15027
+ line.offsetX = line.indent + (align === "center" ? free / 2 : free);
14593
15028
  }
14594
15029
  }
14595
15030
  var TextLayoutEngine = class {
@@ -14599,8 +15034,10 @@ var TextLayoutEngine = class {
14599
15034
  /** Entries by signature, so paragraphs that shifted index are reused. */
14600
15035
  pool = /* @__PURE__ */ new Map();
14601
15036
  lastParagraphsRelaid = 0;
14602
- constructor(measurer = canvasMeasurer) {
15037
+ metrics;
15038
+ constructor(measurer = canvasMeasurer, metrics = approxMetrics) {
14603
15039
  this.measure = measurer;
15040
+ this.metrics = metrics;
14604
15041
  }
14605
15042
  /** Number of paragraphs recomputed by the last `layout()` call. */
14606
15043
  getLastRelaidCount() {
@@ -14611,10 +15048,17 @@ var TextLayoutEngine = class {
14611
15048
  this.paragraphs = [];
14612
15049
  this.pool.clear();
14613
15050
  }
14614
- paragraphSig(block, style, bounds) {
14615
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14616
- const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14617
- return `${fontString(style)}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}|${JSON.stringify(block.runs)}`;
15051
+ /** Text indent of a block in px (0 for plain paragraphs). */
15052
+ indentFor(block, style, fixedWidth, maxWidth) {
15053
+ if (!block.kind) return 0;
15054
+ const stepPx = (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15055
+ const wanted = (Math.max(0, Math.floor(block.depth ?? 0)) + 1) * stepPx;
15056
+ if (!fixedWidth) return wanted;
15057
+ const limit = Math.max(0, maxWidth - 4 * style.fontSize);
15058
+ return Math.min(wanted, limit);
15059
+ }
15060
+ sigFor(block, style, fixedWidth, maxWidth, indent) {
15061
+ return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
14618
15062
  }
14619
15063
  /**
14620
15064
  * Pre-compute paragraph layouts for `blocks` within a time budget so a
@@ -14622,14 +15066,14 @@ var TextLayoutEngine = class {
14622
15066
  * Lets callers time-slice very large content (e.g. bulk paste).
14623
15067
  */
14624
15068
  warm(blocks, style, bounds, budgetMs) {
14625
- const font = fontString(style);
14626
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
15069
+ const fixedWidth = isNum(bounds.width);
14627
15070
  const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14628
15071
  const deadline = now() + budgetMs;
14629
15072
  for (const block of blocks) {
14630
- const sig = this.paragraphSig(block, style, bounds);
15073
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15074
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14631
15075
  if (this.pool.has(sig)) continue;
14632
- this.pool.set(sig, this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth));
15076
+ this.pool.set(sig, this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent));
14633
15077
  if (now() >= deadline) return false;
14634
15078
  }
14635
15079
  return true;
@@ -14647,35 +15091,38 @@ var TextLayoutEngine = class {
14647
15091
  return w + text.length * spacing;
14648
15092
  }
14649
15093
  layout(blocks, style, bounds) {
14650
- const font = fontString(style);
14651
15094
  const padding = bounds.padding;
14652
15095
  const fixedWidth = isNum(bounds.width);
14653
15096
  const fixedHeight = isNum(bounds.height);
14654
15097
  const maxWidth = (bounds.width ?? 0) - padding * 2;
14655
15098
  const maxHeightPx = (bounds.height ?? 0) - padding * 2;
14656
15099
  const lineHeightPx = style.lineHeight * style.fontSize;
14657
- const sigBase = `${font}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}`;
15100
+ const markers = listMarkers(blocks);
14658
15101
  const prev = this.paragraphs;
14659
15102
  const next = [];
14660
15103
  let relaid = 0;
14661
15104
  const lines = [];
14662
15105
  let textWidth = 0;
14663
15106
  let offset = 0;
15107
+ let contentHeight = 0;
14664
15108
  for (let p = 0; p < blocks.length; p++) {
14665
15109
  const block = blocks[p];
14666
- const sig = `${sigBase}|${JSON.stringify(block.runs)}`;
15110
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15111
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14667
15112
  let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
14668
15113
  if (!entry) {
14669
- entry = this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth);
15114
+ entry = this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent);
14670
15115
  relaid++;
14671
15116
  }
14672
15117
  next.push(entry);
14673
15118
  const emitted = this.emitLines(entry, block, p, offset, lines, {
14674
- font,
14675
15119
  style,
14676
- lineHeightPx,
15120
+ indent,
15121
+ marker: markers[p],
15122
+ y: contentHeight,
14677
15123
  maxHeightPx: fixedHeight ? maxHeightPx : void 0
14678
15124
  });
15125
+ contentHeight = emitted.y;
14679
15126
  textWidth = Math.max(textWidth, emitted.maxWidth);
14680
15127
  if (emitted.stopped) break;
14681
15128
  offset += blockText(block).length + 1;
@@ -14684,25 +15131,42 @@ var TextLayoutEngine = class {
14684
15131
  this.pool = new Map(next.map((e) => [e.sig, e]));
14685
15132
  this.lastParagraphsRelaid = relaid;
14686
15133
  const width = fixedWidth ? bounds.width : textWidth + padding * 2;
14687
- const height = fixedHeight ? bounds.height : lines.length * lineHeightPx + padding * 2;
14688
- applyAlign(lines, style.align, width, padding);
15134
+ const height = fixedHeight ? bounds.height : contentHeight + padding * 2;
15135
+ applyAlign(lines, width, padding);
14689
15136
  return {
14690
15137
  lines,
14691
15138
  textWidth,
14692
15139
  textHeight: style.fontSize,
14693
15140
  lineHeightPx,
15141
+ contentHeight,
14694
15142
  width,
14695
15143
  height
14696
15144
  };
14697
15145
  }
14698
15146
  /** Append a paragraph's lines to `lines`; stops once a fixed height is full. */
14699
15147
  emitLines(entry, block, paragraph, offset, lines, opts) {
14700
- const { font, style, lineHeightPx, maxHeightPx } = opts;
15148
+ const { style, indent, maxHeightPx } = opts;
15149
+ let { y } = opts;
14701
15150
  let maxWidth = 0;
15151
+ let first = true;
14702
15152
  for (const pl of entry.lines) {
14703
15153
  const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
14704
- const segments = this.buildSegments(entry, block, pl, font, style);
15154
+ const segments = this.buildSegments(entry, block, pl, style);
14705
15155
  const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
15156
+ let marker;
15157
+ if (first && opts.marker) {
15158
+ const m0 = block.runs[0]?.m;
15159
+ const rs = runStyle(style, m0);
15160
+ const stepPx = Math.min(indent, this.stepPx(style));
15161
+ marker = {
15162
+ text: opts.marker,
15163
+ x: Math.max(0, indent - stepPx),
15164
+ font: fontString(rs),
15165
+ fontSize: rs.fontSize,
15166
+ color: typeof m0?.color === "string" ? m0.color : void 0
15167
+ };
15168
+ }
15169
+ first = false;
14706
15170
  lines.push({
14707
15171
  paragraph,
14708
15172
  start: offset + startChar,
@@ -14710,30 +15174,41 @@ var TextLayoutEngine = class {
14710
15174
  text,
14711
15175
  width: segments.reduce((a, s) => a + s.width, 0),
14712
15176
  measuredWidth: pl.measuredWidth,
14713
- offsetX: 0,
14714
- y: lines.length * lineHeightPx,
15177
+ offsetX: indent,
15178
+ y,
15179
+ height: pl.height,
15180
+ baseline: pl.baseline,
15181
+ indent,
15182
+ marker,
15183
+ align: block.align ?? style.align,
14715
15184
  segments,
14716
15185
  lastInParagraph: pl.lastInParagraph
14717
15186
  });
14718
- maxWidth = Math.max(maxWidth, pl.measuredWidth);
14719
- if (maxHeightPx !== void 0 && (lines.length + 1) * lineHeightPx > maxHeightPx) return {
15187
+ y += pl.height;
15188
+ maxWidth = Math.max(maxWidth, pl.measuredWidth + indent);
15189
+ if (maxHeightPx !== void 0 && y + pl.height > maxHeightPx) return {
14720
15190
  stopped: true,
14721
- maxWidth
15191
+ maxWidth,
15192
+ y
14722
15193
  };
14723
15194
  }
14724
15195
  return {
14725
15196
  stopped: false,
14726
- maxWidth
15197
+ maxWidth,
15198
+ y
14727
15199
  };
14728
15200
  }
14729
- rangeWidth(entry, from, to, font, spacing) {
15201
+ stepPx(style) {
15202
+ return (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15203
+ }
15204
+ rangeWidth(entry, from, to, spacing) {
14730
15205
  let width = 0;
14731
15206
  let i = from;
14732
15207
  while (i < to) {
14733
15208
  const run = entry.runOf[i];
14734
15209
  let j = i;
14735
15210
  while (j < to && entry.runOf[j] === run) j++;
14736
- width += this.measureChunk(font, entry.graphemes.slice(i, j).join(""), spacing);
15211
+ width += this.measureChunk(entry.fonts[run], entry.graphemes.slice(i, j).join(""), spacing);
14737
15212
  i = j;
14738
15213
  }
14739
15214
  return width;
@@ -14769,17 +15244,58 @@ var TextLayoutEngine = class {
14769
15244
  matchWidth
14770
15245
  };
14771
15246
  }
14772
- layoutParagraph(block, sig, font, style, fixedWidth, maxWidth) {
15247
+ layoutParagraph(block, sig, style, fixedWidth, maxWidth) {
15248
+ const entry = this.wrapParagraph(block, sig, style, fixedWidth, maxWidth);
15249
+ this.measureLines(entry, style);
15250
+ return entry;
15251
+ }
15252
+ /** Per-line box height and baseline from the runs on each line. */
15253
+ measureLines(entry, style) {
15254
+ const baseFont = fontString(style);
15255
+ for (const line of entry.lines) {
15256
+ let size = 0;
15257
+ let height = 0;
15258
+ let asc = 0;
15259
+ let desc = 0;
15260
+ const seen = /* @__PURE__ */ new Set();
15261
+ for (let g = line.startG; g < line.endG; g++) {
15262
+ const r = entry.runOf[g];
15263
+ if (seen.has(r)) continue;
15264
+ seen.add(r);
15265
+ size = Math.max(size, entry.sizes[r]);
15266
+ height = Math.max(height, entry.lineHeights[r] * entry.sizes[r]);
15267
+ const m = this.metrics(entry.fonts[r], entry.sizes[r]);
15268
+ asc = Math.max(asc, m.ascent);
15269
+ desc = Math.max(desc, m.descent);
15270
+ }
15271
+ if (seen.size === 0) {
15272
+ size = style.fontSize;
15273
+ height = style.lineHeight * size;
15274
+ const m = this.metrics(baseFont, style.fontSize);
15275
+ asc = m.ascent;
15276
+ desc = m.descent;
15277
+ }
15278
+ line.height = height;
15279
+ line.baseline = (asc - desc) / 2 + line.height / 2;
15280
+ }
15281
+ }
15282
+ wrapParagraph(block, sig, style, fixedWidth, maxWidth) {
14773
15283
  const { graphemes, runOf } = flattenBlock(block);
15284
+ const baseFont = fontString(style);
15285
+ const fonts = block.runs.map((r) => r.m ? fontString(runStyle(style, r.m)) : baseFont);
15286
+ const sizes = block.runs.map((r) => style.fontSize * runRatio(r.m));
14774
15287
  const entry = {
14775
15288
  sig,
14776
15289
  graphemes,
14777
15290
  runOf,
15291
+ fonts,
15292
+ lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
15293
+ sizes,
14778
15294
  lines: []
14779
15295
  };
14780
15296
  const n = graphemes.length;
14781
15297
  const spacing = style.letterSpacing;
14782
- const width = (a, b) => this.rangeWidth(entry, a, b, font, spacing);
15298
+ const width = (a, b) => this.rangeWidth(entry, a, b, spacing);
14783
15299
  const shouldWrap = style.wrap !== "none";
14784
15300
  const wrapAtWord = style.wrap !== "char" && shouldWrap;
14785
15301
  const whole = width(0, n);
@@ -14788,7 +15304,9 @@ var TextLayoutEngine = class {
14788
15304
  startG: 0,
14789
15305
  endG: n,
14790
15306
  measuredWidth: whole,
14791
- lastInParagraph: true
15307
+ lastInParagraph: true,
15308
+ height: 0,
15309
+ baseline: 0
14792
15310
  });
14793
15311
  return entry;
14794
15312
  }
@@ -14800,7 +15318,9 @@ var TextLayoutEngine = class {
14800
15318
  startG: start,
14801
15319
  endG: n,
14802
15320
  measuredWidth: matchWidth,
14803
- lastInParagraph: true
15321
+ lastInParagraph: true,
15322
+ height: 0,
15323
+ baseline: 0
14804
15324
  });
14805
15325
  return entry;
14806
15326
  }
@@ -14816,7 +15336,9 @@ var TextLayoutEngine = class {
14816
15336
  startG: start,
14817
15337
  endG: trimTrailingSpace(graphemes, start, end),
14818
15338
  measuredWidth: matchWidth,
14819
- lastInParagraph: false
15339
+ lastInParagraph: false,
15340
+ height: 0,
15341
+ baseline: 0
14820
15342
  });
14821
15343
  start = skipSpace(graphemes, end);
14822
15344
  }
@@ -14824,7 +15346,7 @@ var TextLayoutEngine = class {
14824
15346
  if (last) last.lastInParagraph = true;
14825
15347
  return entry;
14826
15348
  }
14827
- buildSegments(entry, block, pl, font, style) {
15349
+ buildSegments(entry, block, pl, style) {
14828
15350
  const segments = [];
14829
15351
  let x = 0;
14830
15352
  let i = pl.startG;
@@ -14833,12 +15355,16 @@ var TextLayoutEngine = class {
14833
15355
  let j = i;
14834
15356
  while (j < pl.endG && entry.runOf[j] === run) j++;
14835
15357
  const t = entry.graphemes.slice(i, j).join("");
15358
+ const font = entry.fonts[run];
14836
15359
  const width = this.measureChunk(font, t, style.letterSpacing);
14837
15360
  segments.push({
14838
15361
  t,
14839
15362
  m: block.runs[run].m,
15363
+ run,
14840
15364
  x,
14841
- width
15365
+ width,
15366
+ font,
15367
+ fontSize: entry.sizes[run]
14842
15368
  });
14843
15369
  x += width;
14844
15370
  i = j;
@@ -14870,12 +15396,18 @@ const LAYOUT_ATTRS = [
14870
15396
  * code written against `Konva.Text` keeps working.
14871
15397
  */
14872
15398
  var WeaveTextV2Shape = class extends Konva.Shape {
14873
- engine = new TextLayoutEngine();
14874
- naturalEngine = new TextLayoutEngine();
15399
+ engine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
15400
+ naturalEngine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
14875
15401
  layoutCache = null;
14876
15402
  blocksKey = {};
14877
15403
  blocksCache = [];
14878
15404
  viewport = null;
15405
+ indentStepValue;
15406
+ linkStyle = {
15407
+ color: "#1155ccff",
15408
+ hoverColor: "#3d7be0ff"
15409
+ };
15410
+ hoveredRun = null;
14879
15411
  constructor(config) {
14880
15412
  super(config);
14881
15413
  this.className = "WeaveTextV2Shape";
@@ -14892,6 +15424,55 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14892
15424
  this._setAttr("text", value == null ? "" : String(value));
14893
15425
  return this;
14894
15426
  }
15427
+ /**
15428
+ * List indent per depth, as a multiple of `fontSize`. Not a node attr: it
15429
+ * comes from the node config and must not be serialized into the document.
15430
+ */
15431
+ setIndentStep(step) {
15432
+ if (this.indentStepValue !== step) {
15433
+ this.indentStepValue = step;
15434
+ this.layoutCache = null;
15435
+ }
15436
+ return this;
15437
+ }
15438
+ /** Link run colours (from the node config; not serialized). */
15439
+ setLinkStyle(style) {
15440
+ this.linkStyle = style;
15441
+ return this;
15442
+ }
15443
+ /** Run under the pointer (`paragraph:run`), repainted with the hover colour. */
15444
+ setHoveredRun(key) {
15445
+ if (this.hoveredRun !== key) {
15446
+ this.hoveredRun = key;
15447
+ this.getLayer()?.batchDraw();
15448
+ }
15449
+ return this;
15450
+ }
15451
+ getHoveredRun() {
15452
+ return this.hoveredRun;
15453
+ }
15454
+ /**
15455
+ * Link run at a point in node-local coordinates (the legacy node-level
15456
+ * `link` wins: runs are then not links).
15457
+ */
15458
+ linkAtPoint(point) {
15459
+ if (this.attrs.link) return null;
15460
+ const layout = this.ensureLayout();
15461
+ const o = this.getContentOffset();
15462
+ const x = point.x - o.x;
15463
+ const y = point.y - o.y;
15464
+ for (const line of layout.lines) {
15465
+ if (y < line.y || y >= line.y + line.height) continue;
15466
+ for (const seg of line.segments) {
15467
+ const sx = line.offsetX + seg.x;
15468
+ if (x >= sx && x <= sx + seg.width && isSafeLink(seg.m?.link)) return {
15469
+ key: `${line.paragraph}:${seg.run}`,
15470
+ url: seg.m.link
15471
+ };
15472
+ }
15473
+ }
15474
+ return null;
15475
+ }
14895
15476
  /** Resolved blocks: stored or lazily synthesized (never written back). */
14896
15477
  getBlocks() {
14897
15478
  const k = this.blocksKey;
@@ -14912,6 +15493,10 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14912
15493
  }
14913
15494
  /** Replace content with blocks, keeping the `text` mirror in sync. */
14914
15495
  setBlocks(blocks) {
15496
+ if (!Array.isArray(blocks)) {
15497
+ this._setAttr("blocks", void 0);
15498
+ return this;
15499
+ }
14915
15500
  this._setAttr("blocks", blocks);
14916
15501
  this._setAttr("text", blocksToText(blocks));
14917
15502
  return this;
@@ -14949,6 +15534,16 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14949
15534
  textDecoration(v) {
14950
15535
  return this.accessor("textDecoration", "", v);
14951
15536
  }
15537
+ /**
15538
+ * Always stroke under the node transform. Weave turns `strokeScaleEnabled`
15539
+ * off while a node is transformed; Konva then resets the canvas transform
15540
+ * before stroking, which only works for path shapes. `strokeText` is not a
15541
+ * path, so the outline was painted at local coordinates on the canvas
15542
+ * origin at stage scale 1 (visible over other nodes while resizing).
15543
+ */
15544
+ getStrokeScaleEnabled() {
15545
+ return true;
15546
+ }
14952
15547
  accessor(name, fallback, value) {
14953
15548
  if (value === void 0) return this.attrs[name] ?? fallback;
14954
15549
  this.setAttr(name, value);
@@ -14983,7 +15578,8 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14983
15578
  lineHeight: this.lineHeight(),
14984
15579
  letterSpacing: this.letterSpacing(),
14985
15580
  wrap: this.wrap(),
14986
- align: this.align()
15581
+ align: this.align(),
15582
+ indentStep: this.indentStepValue
14987
15583
  };
14988
15584
  }
14989
15585
  /** Lazy, cached layout. Size accessors and painting force it. */
@@ -15013,14 +15609,13 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15013
15609
  };
15014
15610
  }
15015
15611
  /** Rendered width of a string with this shape's font and letter spacing. */
15016
- measureText(text) {
15017
- return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
15612
+ measureText(text, font) {
15613
+ return canvasMeasurer(font ?? fontString(this.style()), text) + text.length * this.letterSpacing();
15018
15614
  }
15019
15615
  /** Offset of the first line inside the box (padding + vertical align). */
15020
15616
  getContentOffset() {
15021
15617
  const padding = this.padding();
15022
- const layout = this.ensureLayout();
15023
- const used = layout.lines.length * layout.lineHeightPx + padding * 2;
15618
+ const used = this.ensureLayout().contentHeight + padding * 2;
15024
15619
  const va = this.verticalAlign();
15025
15620
  let alignY = 0;
15026
15621
  if (va === "middle") alignY = (this.getHeight() - used) / 2;
@@ -15058,8 +15653,9 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15058
15653
  const det = m.a * m.d - m.b * m.c;
15059
15654
  if (!det || !Number.isFinite(det)) return null;
15060
15655
  const canvas = ctx.getCanvas();
15061
- const w = canvas.getWidth();
15062
- const h = canvas.getHeight();
15656
+ const backing = canvas._canvas;
15657
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15658
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15063
15659
  if (!(w > 0) || !(h > 0)) return null;
15064
15660
  let y1 = Infinity;
15065
15661
  let y2 = -Infinity;
@@ -15083,30 +15679,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15083
15679
  const layout = self.ensureLayout();
15084
15680
  if (layout.lines.length === 0) return;
15085
15681
  const padding = self.padding();
15086
- const lineHeightPx = layout.lineHeightPx;
15087
15682
  let baseline = "middle";
15088
- let translateY = lineHeightPx / 2;
15089
- if (!Konva.legacyTextRendering) {
15090
- const m = self.measureMetrics();
15091
- baseline = "alphabetic";
15092
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15093
- }
15683
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15094
15684
  ctx.setAttr("font", fontString(self.style()));
15095
15685
  ctx.setAttr("textBaseline", baseline);
15096
15686
  ctx.setAttr("textAlign", "left");
15097
15687
  const alignY = self.verticalOffset(layout);
15098
15688
  const range = self.visibleRange(ctx);
15099
15689
  ctx.translate(padding, alignY + padding);
15100
- const top = alignY + padding;
15101
- for (let n = 0; n < layout.lines.length; n++) {
15102
- const lineTop = top + n * lineHeightPx;
15103
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15104
- self.paintLine(ctx, layout.lines[n], translateY + n * lineHeightPx);
15690
+ for (const line of layout.lines) {
15691
+ if (range && (alignY + padding + line.y + line.height < range.y1 - line.height || alignY + padding + line.y > range.y2 + line.height)) continue;
15692
+ self.paintLine(ctx, line, line.y + line.baseline);
15105
15693
  }
15106
15694
  };
15107
15695
  /** Vertical offset of the text block inside the box (verticalAlign). */
15108
15696
  verticalOffset(layout) {
15109
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15697
+ const used = layout.contentHeight + this.padding() * 2;
15110
15698
  const va = this.verticalAlign();
15111
15699
  if (va === "middle") return (this.getHeight() - used) / 2;
15112
15700
  if (va === "bottom") return this.getHeight() - used;
@@ -15117,7 +15705,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15117
15705
  const fontSize = this.fontSize();
15118
15706
  ctx.save();
15119
15707
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15120
- for (const seg of line.segments) this.paintSegment(ctx, seg.t, line.offsetX + seg.x, y);
15708
+ if (line.marker) {
15709
+ const mk = line.marker;
15710
+ ctx.setAttr("font", mk.font);
15711
+ this.fillText(ctx, mk.text, mk.x, y, mk.color);
15712
+ }
15713
+ for (const seg of line.segments) {
15714
+ ctx.setAttr("font", seg.font);
15715
+ const x = line.offsetX + seg.x;
15716
+ const isLink = !this.attrs.link && isSafeLink(seg.m?.link);
15717
+ let color = typeof seg.m?.color === "string" ? seg.m.color : void 0;
15718
+ if (isLink) color = this.hoveredRun === `${line.paragraph}:${seg.run}` ? this.linkStyle.hoverColor : this.linkStyle.color;
15719
+ this.paintSegment(ctx, seg, x, y, color);
15720
+ if (seg.m?.underline || isLink) this.strokeSegment(ctx, seg, x, y + Math.round(seg.fontSize / 4), color);
15721
+ if (seg.m?.strike) this.strokeSegment(ctx, seg, x, y - Math.round(seg.fontSize / 4), color);
15722
+ }
15121
15723
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15122
15724
  ctx.restore();
15123
15725
  }
@@ -15131,24 +15733,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15131
15733
  ctx.stroke();
15132
15734
  ctx.restore();
15133
15735
  }
15134
- paintSegment(ctx, text, x, y) {
15736
+ strokeSegment(ctx, seg, x, y, color) {
15737
+ ctx.save();
15738
+ ctx.beginPath();
15739
+ ctx.moveTo(x, y);
15740
+ ctx.lineTo(x + Math.round(seg.width), y);
15741
+ ctx.lineWidth = seg.fontSize / 15;
15742
+ ctx.strokeStyle = color ?? this.fill();
15743
+ ctx.stroke();
15744
+ ctx.restore();
15745
+ }
15746
+ paintSegment(ctx, seg, x, y, color) {
15135
15747
  const spacing = this.letterSpacing();
15136
15748
  if (spacing === 0) {
15137
- this.fillText(ctx, text, x, y);
15749
+ this.fillText(ctx, seg.t, x, y, color);
15138
15750
  return;
15139
15751
  }
15140
15752
  let cx = x;
15141
- for (const ch of Array.from(text)) {
15142
- this.fillText(ctx, ch, cx, y);
15143
- cx += this.measureSize(ch).width + spacing;
15753
+ for (const ch of Array.from(seg.t)) {
15754
+ this.fillText(ctx, ch, cx, y, color);
15755
+ cx += canvasMeasurer(seg.font, ch) + spacing;
15144
15756
  }
15145
15757
  }
15146
- fillText(ctx, text, x, y) {
15758
+ fillText(ctx, text, x, y, color) {
15147
15759
  const self = this;
15148
15760
  self._partialText = text;
15149
15761
  self._partialTextX = x;
15150
15762
  self._partialTextY = y;
15151
- ctx.fillStrokeShape(this);
15763
+ if (color === void 0) {
15764
+ ctx.fillStrokeShape(this);
15765
+ return;
15766
+ }
15767
+ const prev = this.attrs.fill;
15768
+ this.attrs.fill = color;
15769
+ try {
15770
+ ctx.fillStrokeShape(this);
15771
+ } finally {
15772
+ this.attrs.fill = prev;
15773
+ }
15152
15774
  }
15153
15775
  _hitFunc = (ctx, shape) => {
15154
15776
  ctx.beginPath();
@@ -15264,9 +15886,27 @@ function lineIndexAt(layout, offset) {
15264
15886
  }
15265
15887
  return fallback;
15266
15888
  }
15889
+ /** Width of the first `k` UTF-16 units of a line, run by run. */
15890
+ function prefixWidth(line, k, measure) {
15891
+ if (k <= 0) return 0;
15892
+ if (line.segments.length === 0) return measure(line.text.slice(0, k));
15893
+ let width = 0;
15894
+ let pos = 0;
15895
+ for (const seg of line.segments) {
15896
+ const end = pos + seg.t.length;
15897
+ if (k >= end) width += seg.width;
15898
+ else {
15899
+ width += measure(seg.t.slice(0, k - pos), seg.font);
15900
+ break;
15901
+ }
15902
+ pos = end;
15903
+ if (k === end) break;
15904
+ }
15905
+ return width;
15906
+ }
15267
15907
  function lineX(line, offset, measure) {
15268
15908
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15269
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15909
+ return line.offsetX + prefixWidth(line, k, measure);
15270
15910
  }
15271
15911
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15272
15912
  function caretRect(layout, offset, measure) {
@@ -15280,16 +15920,22 @@ function caretRect(layout, offset, measure) {
15280
15920
  };
15281
15921
  return {
15282
15922
  x: lineX(line, offset, measure),
15283
- y: i * layout.lineHeightPx,
15284
- height: layout.lineHeightPx,
15923
+ y: line.y,
15924
+ height: line.height,
15285
15925
  line: i
15286
15926
  };
15287
15927
  }
15928
+ /** Index of the line containing a local y (clamped). */
15929
+ function lineIndexAtY(layout, y) {
15930
+ const lines = layout.lines;
15931
+ for (let i = 0; i < lines.length; i++) if (y < lines[i].y + lines[i].height) return i;
15932
+ return Math.max(0, lines.length - 1);
15933
+ }
15288
15934
  /** Offset closest to a point in text-local coordinates. */
15289
15935
  function offsetFromPoint(layout, point, measure) {
15290
15936
  const lines = layout.lines;
15291
15937
  if (lines.length === 0) return 0;
15292
- return offsetInLine(lines[Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / layout.lineHeightPx)))], point.x, measure);
15938
+ return offsetInLine(lines[lineIndexAtY(layout, point.y)], point.x, measure);
15293
15939
  }
15294
15940
  function offsetInLine(line, x, measure) {
15295
15941
  const rel = x - line.offsetX;
@@ -15297,7 +15943,7 @@ function offsetInLine(line, x, measure) {
15297
15943
  const b = boundaries(line.text, graphemeSeg);
15298
15944
  let prevW = 0;
15299
15945
  for (let k = 1; k < b.length; k++) {
15300
- const w = measure(line.text.slice(0, b[k]));
15946
+ const w = prefixWidth(line, b[k], measure);
15301
15947
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15302
15948
  prevW = w;
15303
15949
  }
@@ -15321,7 +15967,7 @@ function selectionRects(layout, from, to, measure) {
15321
15967
  if (from > to) [from, to] = [to, from];
15322
15968
  if (from === to) return [];
15323
15969
  const out = [];
15324
- layout.lines.forEach((line, i) => {
15970
+ layout.lines.forEach((line) => {
15325
15971
  const hasNewline = line.lastInParagraph;
15326
15972
  const lineFrom = Math.max(from, line.start);
15327
15973
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15332,9 +15978,9 @@ function selectionRects(layout, from, to, measure) {
15332
15978
  if (x2 <= x1) return;
15333
15979
  out.push({
15334
15980
  x: x1,
15335
- y: i * layout.lineHeightPx,
15981
+ y: line.y,
15336
15982
  width: x2 - x1,
15337
- height: layout.lineHeightPx
15983
+ height: line.height
15338
15984
  });
15339
15985
  });
15340
15986
  return out;
@@ -15353,6 +15999,11 @@ var TextEditModel = class {
15353
15999
  focus = 0;
15354
16000
  /** x kept while moving vertically; reset by any other move. */
15355
16001
  desiredX = null;
16002
+ /**
16003
+ * Marks the next typed text takes at a collapsed caret (e.g. bold toggled
16004
+ * with nothing selected). Cleared when the selection moves.
16005
+ */
16006
+ storedMarks = null;
15356
16007
  undoStack = [];
15357
16008
  redoStack = [];
15358
16009
  lastKind = null;
@@ -15389,12 +16040,14 @@ var TextEditModel = class {
15389
16040
  const len = this.getText().length;
15390
16041
  this.anchor = Math.max(0, Math.min(anchor, len));
15391
16042
  this.focus = Math.max(0, Math.min(focus, len));
16043
+ this.storedMarks = null;
15392
16044
  this.breakCoalescing();
15393
16045
  }
15394
16046
  moveTo(offset, extend = false, keepDesiredX = false) {
15395
16047
  const len = this.getText().length;
15396
16048
  const o = Math.max(0, Math.min(offset, len));
15397
16049
  this.focus = o;
16050
+ this.storedMarks = null;
15398
16051
  if (!extend) this.anchor = o;
15399
16052
  if (!keepDesiredX) this.desiredX = null;
15400
16053
  this.breakCoalescing();
@@ -15402,6 +16055,7 @@ var TextEditModel = class {
15402
16055
  selectAll() {
15403
16056
  this.anchor = 0;
15404
16057
  this.focus = this.getText().length;
16058
+ this.storedMarks = null;
15405
16059
  this.breakCoalescing();
15406
16060
  }
15407
16061
  breakCoalescing() {
@@ -15437,7 +16091,14 @@ var TextEditModel = class {
15437
16091
  end = Math.max(start, Math.min(end, len));
15438
16092
  if (start === end && text === "") return false;
15439
16093
  this.record(kind, start);
15440
- this.blocks = replaceRange(this.blocks, start, end, text);
16094
+ const carried = this.storedMarks ?? marksAtOffset(this.blocks, start);
16095
+ this.blocks = replaceRange(this.blocks, start, end, text, this.storedMarks ?? void 0);
16096
+ this.storedMarks = null;
16097
+ if (text.endsWith("\n") && carried) {
16098
+ const { link: _link, ...rest } = carried;
16099
+ const next = cleanMarks(rest);
16100
+ if (next && !marksAtOffset(this.blocks, start + text.length)) this.storedMarks = next;
16101
+ }
15441
16102
  this.anchor = this.focus = start + text.length;
15442
16103
  this.lastEditEnd = this.focus;
15443
16104
  this.desiredX = null;
@@ -15477,14 +16138,102 @@ var TextEditModel = class {
15477
16138
  this.restore(next);
15478
16139
  return true;
15479
16140
  }
16141
+ /** Current marks at the caret / selection start (stored marks win). */
16142
+ caretMarks() {
16143
+ return this.storedMarks ?? marksAtOffset(this.blocks, this.getSelection().start);
16144
+ }
16145
+ /** Always one new undo entry, never coalesced. */
16146
+ commitFormat(next) {
16147
+ if (JSON.stringify(next) === JSON.stringify(this.blocks)) return false;
16148
+ this.lastKind = null;
16149
+ this.record("format", -1);
16150
+ this.lastKind = null;
16151
+ this.blocks = next;
16152
+ return true;
16153
+ }
16154
+ /** Set (`undefined` clears) one mark on the selection or the stored marks. */
16155
+ setMark(key, value) {
16156
+ const { start, end } = this.getSelection();
16157
+ if (start === end) {
16158
+ const base = { ...this.caretMarks() ?? {} };
16159
+ if (value === void 0 || value === false) delete base[key];
16160
+ else base[key] = value;
16161
+ this.storedMarks = cleanMarks(base) ?? {};
16162
+ return true;
16163
+ }
16164
+ return this.commitFormat(applyMark(this.blocks, start, end, key, value));
16165
+ }
16166
+ /** Toggle a boolean mark: on unless the whole selection already has it. */
16167
+ toggleMark(key) {
16168
+ const { start, end } = this.getSelection();
16169
+ const q = queryMark(this.blocks, start, end, key, this.storedMarks);
16170
+ return this.setMark(key, q.state === "all" ? void 0 : true);
16171
+ }
16172
+ toggleList(kind) {
16173
+ const { start, end } = this.getSelection();
16174
+ return this.commitFormat(toggleList(this.blocks, start, end, kind));
16175
+ }
16176
+ setAlign(align) {
16177
+ const { start, end } = this.getSelection();
16178
+ return this.commitFormat(setBlockAlign(this.blocks, start, end, align));
16179
+ }
16180
+ indent() {
16181
+ const { start, end } = this.getSelection();
16182
+ return this.commitFormat(indentBlocks(this.blocks, start, end));
16183
+ }
16184
+ outdent() {
16185
+ const { start, end } = this.getSelection();
16186
+ return this.commitFormat(outdentBlocks(this.blocks, start, end));
16187
+ }
16188
+ blockAt(offset) {
16189
+ let pos = 0;
16190
+ for (let i = 0; i < this.blocks.length; i++) {
16191
+ const len = blockText(this.blocks[i]).length;
16192
+ if (offset <= pos + len) return {
16193
+ index: i,
16194
+ start: pos,
16195
+ length: len
16196
+ };
16197
+ pos += len + 1;
16198
+ }
16199
+ }
16200
+ /**
16201
+ * Enter in a list: an empty item exits the list; otherwise the item
16202
+ * continues (the block split carries `kind` and `depth`).
16203
+ */
16204
+ enter() {
16205
+ const { start, end } = this.getSelection();
16206
+ const b = start === end ? this.blockAt(start) : void 0;
16207
+ if (b && this.blocks[b.index].kind && b.length === 0) return this.commitFormat(outdentToPlain(this.blocks, b.index));
16208
+ return this.replaceRange(start, end, "\n", "other");
16209
+ }
16210
+ /**
16211
+ * Backspace at the start of a list item outdents it, or removes the
16212
+ * marker at depth 0. Returns false when the key is not handled here.
16213
+ */
16214
+ backspaceInList() {
16215
+ const { start, end } = this.getSelection();
16216
+ if (start !== end) return false;
16217
+ const b = this.blockAt(start);
16218
+ if (!b || b.start !== start || !this.blocks[b.index].kind) return false;
16219
+ return this.commitFormat(outdentBlocks(this.blocks, start, start));
16220
+ }
15480
16221
  restore(s) {
15481
16222
  this.blocks = s.blocks;
16223
+ this.storedMarks = null;
15482
16224
  this.anchor = s.anchor;
15483
16225
  this.focus = s.focus;
15484
16226
  this.lastKind = null;
15485
16227
  this.desiredX = null;
15486
16228
  }
15487
16229
  };
16230
+ function outdentToPlain(blocks, index) {
16231
+ return blocks.map((b, i) => {
16232
+ if (i !== index) return b;
16233
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
16234
+ return rest;
16235
+ });
16236
+ }
15488
16237
  const SLICE_BUDGET_MS = 8;
15489
16238
  const BLINK_MS = 530;
15490
16239
  var TextEditSession = class {
@@ -15542,7 +16291,7 @@ var TextEditSession = class {
15542
16291
  };
15543
16292
  if (this.host.selectAllOnStart) this.model.selectAll();
15544
16293
  this.syncTextarea();
15545
- ta.focus();
16294
+ ta.focus({ preventScroll: true });
15546
16295
  this.positionTextarea();
15547
16296
  this.startBlink();
15548
16297
  this.repaint();
@@ -15571,7 +16320,7 @@ var TextEditSession = class {
15571
16320
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15572
16321
  }
15573
16322
  focus() {
15574
- this.textarea?.focus();
16323
+ this.textarea?.focus({ preventScroll: true });
15575
16324
  }
15576
16325
  createTextarea() {
15577
16326
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15620,8 +16369,12 @@ var TextEditSession = class {
15620
16369
  if (!ta) return;
15621
16370
  const c = this.caretStagePoint();
15622
16371
  const p = this.host.stageToContainer(c);
15623
- ta.style.left = `${p.x}px`;
15624
- ta.style.top = `${p.y}px`;
16372
+ this.host.revealCaret?.(p);
16373
+ const box = this.host.container;
16374
+ const maxX = Math.max(0, (box.clientWidth || Infinity) - 2);
16375
+ const maxY = Math.max(0, (box.clientHeight || Infinity) - 20);
16376
+ ta.style.left = `${Math.min(Math.max(p.x, 0), maxX)}px`;
16377
+ ta.style.top = `${Math.min(Math.max(p.y, 0), maxY)}px`;
15625
16378
  }
15626
16379
  caretStagePoint() {
15627
16380
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15653,10 +16406,41 @@ var TextEditSession = class {
15653
16406
  if (e.key === "Escape") this.host.onRequestExit("escape");
15654
16407
  else if (isUndo) this.undo();
15655
16408
  else if (isRedo) this.redo();
16409
+ else if (this.handleRichKey(e, mod, key)) return true;
15656
16410
  else if (e.key === "Tab") return true;
15657
16411
  else return false;
15658
16412
  return true;
15659
16413
  }
16414
+ /** Format shortcuts and list key rules; only when rich text is enabled. */
16415
+ handleRichKey(e, mod, key) {
16416
+ if (!this.host.richText) return false;
16417
+ if (mod && !e.shiftKey && !e.altKey) {
16418
+ const marks = {
16419
+ b: "bold",
16420
+ i: "italic",
16421
+ u: "underline"
16422
+ };
16423
+ if (marks[key]) {
16424
+ this.toggleMark(marks[key]);
16425
+ return true;
16426
+ }
16427
+ }
16428
+ if (mod || this.composing) return false;
16429
+ if (e.key === "Tab") {
16430
+ if (e.shiftKey) this.outdent();
16431
+ else this.indent();
16432
+ return true;
16433
+ }
16434
+ if (e.key === "Enter") {
16435
+ if (this.model.enter()) this.afterFormat("command", true);
16436
+ return true;
16437
+ }
16438
+ if (e.key === "Backspace" && this.model.backspaceInList()) {
16439
+ this.afterFormat("command", true);
16440
+ return true;
16441
+ }
16442
+ return false;
16443
+ }
15660
16444
  handleNavigationKey(e) {
15661
16445
  if (e.ctrlKey || e.metaKey) return false;
15662
16446
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15710,7 +16494,15 @@ var TextEditSession = class {
15710
16494
  while (p < max && oldText[p] === newText[p]) p++;
15711
16495
  let s = 0;
15712
16496
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15713
- const inserted = newText.slice(p, newText.length - s);
16497
+ let inserted = newText.slice(p, newText.length - s);
16498
+ if (s + p === oldText.length && inserted !== "") {
16499
+ const q = ta.selectionEnd - inserted.length;
16500
+ if (q >= 0 && q < p && newText.slice(q, q + inserted.length) === inserted && oldText.slice(0, q) + inserted + oldText.slice(q) === newText) {
16501
+ s = oldText.length - q;
16502
+ p = q;
16503
+ inserted = newText.slice(p, newText.length - s);
16504
+ }
16505
+ }
15714
16506
  const inputType = e.inputType ?? "";
15715
16507
  const kind = this.editKindFor(inputType);
15716
16508
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15751,6 +16543,7 @@ var TextEditSession = class {
15751
16543
  if (this.model.undo()) {
15752
16544
  this.syncTextarea();
15753
16545
  this.afterContentChange(0, true);
16546
+ this.host.onFormatChange?.("undo");
15754
16547
  }
15755
16548
  }
15756
16549
  redo() {
@@ -15758,8 +16551,42 @@ var TextEditSession = class {
15758
16551
  if (this.model.redo()) {
15759
16552
  this.syncTextarea();
15760
16553
  this.afterContentChange(0, true);
16554
+ this.host.onFormatChange?.("redo");
15761
16555
  }
15762
16556
  }
16557
+ afterFormat(reason, content = false) {
16558
+ this.flushPending();
16559
+ this.syncTextarea();
16560
+ if (content) this.afterContentChange(0, true);
16561
+ else this.applyContent();
16562
+ this.afterSelectionChange();
16563
+ this.host.onFormatChange?.(reason);
16564
+ }
16565
+ toggleMark(key) {
16566
+ if (this.model.toggleMark(key)) this.afterFormat("command");
16567
+ }
16568
+ setMark(key, value) {
16569
+ if (this.model.setMark(key, value)) this.afterFormat("command");
16570
+ }
16571
+ toggleList(kind) {
16572
+ if (this.model.toggleList(kind)) this.afterFormat("command");
16573
+ }
16574
+ setAlign(align) {
16575
+ if (this.model.setAlign(align)) this.afterFormat("command");
16576
+ }
16577
+ indent() {
16578
+ if (this.model.indent()) this.afterFormat("command");
16579
+ }
16580
+ outdent() {
16581
+ if (this.model.outdent()) this.afterFormat("command");
16582
+ }
16583
+ getSelection() {
16584
+ return this.model.getSelection();
16585
+ }
16586
+ getFormat() {
16587
+ const { start, end } = this.model.getSelection();
16588
+ return getSelectionFormat(this.model.getBlocks(), start, end, this.target.getFontSize(), this.model.storedMarks);
16589
+ }
15763
16590
  toLocal(e) {
15764
16591
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15765
16592
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15774,12 +16601,26 @@ var TextEditSession = class {
15774
16601
  const { width, height } = this.target.getSize();
15775
16602
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15776
16603
  }
16604
+ /**
16605
+ * True for pointer targets inside an element the app marked with
16606
+ * `data-weave-text-edit-keep` (e.g. a formatting toolbar): they neither
16607
+ * end the session nor, unless they are form fields, steal focus.
16608
+ */
16609
+ isKeepTarget(e) {
16610
+ const t = e.target;
16611
+ return typeof Element !== "undefined" && t instanceof Element && !!t.closest("[data-weave-text-edit-keep]");
16612
+ }
15777
16613
  onMouseDown = (e) => {
16614
+ if (this.isKeepTarget(e)) {
16615
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16616
+ return;
16617
+ }
15778
16618
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15779
16619
  };
15780
16620
  onPointerDown = (e) => {
15781
16621
  if (e.button !== 0 && e.pointerType === "mouse") return;
15782
16622
  if (this.textarea && e.target === this.textarea) return;
16623
+ if (this.isKeepTarget(e)) return;
15783
16624
  const local = this.toLocal(e);
15784
16625
  if (!this.isInside(local)) {
15785
16626
  e.stopPropagation();
@@ -15790,10 +16631,11 @@ var TextEditSession = class {
15790
16631
  e.preventDefault();
15791
16632
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15792
16633
  const text = this.model.getText();
15793
- if (e.detail >= 3) {
16634
+ const clicks = this.countClicks(e);
16635
+ if (clicks >= 3) {
15794
16636
  const [s, en] = paragraphRangeAt(text, offset);
15795
16637
  this.model.setSelection(s, en);
15796
- } else if (e.detail === 2) {
16638
+ } else if (clicks === 2) {
15797
16639
  const [s, en] = wordRangeAt(text, offset);
15798
16640
  this.model.setSelection(s, en);
15799
16641
  } else {
@@ -15802,8 +16644,31 @@ var TextEditSession = class {
15802
16644
  }
15803
16645
  this.syncTextarea();
15804
16646
  this.afterSelectionChange();
15805
- this.textarea?.focus();
16647
+ this.textarea?.focus({ preventScroll: true });
15806
16648
  };
16649
+ lastClick = {
16650
+ t: 0,
16651
+ x: 0,
16652
+ y: 0,
16653
+ n: 0
16654
+ };
16655
+ /**
16656
+ * Browsers report `detail` as 0 on pointerdown, so multi-click is counted
16657
+ * here (same spot, within 500 ms). A real `detail` still wins.
16658
+ */
16659
+ countClicks(e) {
16660
+ const now = e.timeStamp || Date.now();
16661
+ const l = this.lastClick;
16662
+ const near = now - l.t < 500 && Math.hypot(e.clientX - l.x, e.clientY - l.y) < 5 && l.n > 0;
16663
+ const n = Math.max(e.detail, near ? l.n + 1 : 1);
16664
+ this.lastClick = {
16665
+ t: now,
16666
+ x: e.clientX,
16667
+ y: e.clientY,
16668
+ n: n >= 3 ? 0 : n
16669
+ };
16670
+ return n;
16671
+ }
15807
16672
  onPointerMove = (e) => {
15808
16673
  if (!this.dragging) return;
15809
16674
  const local = this.toLocal(e);
@@ -15815,13 +16680,14 @@ var TextEditSession = class {
15815
16680
  onPointerUp = () => {
15816
16681
  if (!this.dragging) return;
15817
16682
  this.dragging = false;
15818
- this.textarea?.focus();
16683
+ this.textarea?.focus({ preventScroll: true });
15819
16684
  };
15820
16685
  afterSelectionChange() {
15821
16686
  this.syncTextarea();
15822
16687
  this.positionTextarea();
15823
16688
  this.resetBlink();
15824
16689
  this.repaint();
16690
+ this.host.onSelectionChange?.();
15825
16691
  }
15826
16692
  afterContentChange(insertedLength, immediate = false) {
15827
16693
  this.flushPending();
@@ -15845,6 +16711,7 @@ var TextEditSession = class {
15845
16711
  applyContent() {
15846
16712
  this.target.setBlocks(this.model.getBlocks());
15847
16713
  this.host.onChange?.(this.model.getBlocks());
16714
+ this.host.onSelectionChange?.();
15848
16715
  this.positionTextarea();
15849
16716
  this.target.requestRedraw();
15850
16717
  this.repaint();
@@ -15920,15 +16787,29 @@ function host_signal(signal, abort) {
15920
16787
  //#region src/nodes/text-v2/constants.ts
15921
16788
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15922
16789
  const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
16790
+ /** Default list indent per depth, as a multiple of the node `fontSize`. */
16791
+ const WEAVE_TEXT_V2_DEFAULT_INDENT_STEP = 1.5;
15923
16792
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15924
16793
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15925
16794
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15926
- edition: { outline: {
15927
- enabled: true,
15928
- color: "#0074ff",
15929
- width: 1,
15930
- padding: 0
15931
- } },
16795
+ edition: {
16796
+ outline: {
16797
+ enabled: true,
16798
+ color: "#0074ff",
16799
+ width: 1,
16800
+ padding: 0
16801
+ },
16802
+ followPadding: {
16803
+ top: 40,
16804
+ right: 40,
16805
+ bottom: 40,
16806
+ left: 40
16807
+ }
16808
+ },
16809
+ richText: {
16810
+ enabled: false,
16811
+ indentStep: WEAVE_TEXT_V2_DEFAULT_INDENT_STEP
16812
+ },
15932
16813
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15933
16814
  };
15934
16815
  //#endregion
@@ -15956,6 +16837,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15956
16837
  editingNodeId = null;
15957
16838
  session = null;
15958
16839
  sizeWarned = false;
16840
+ selectionTimer;
15959
16841
  nodeRenderedAddedRegistered = false;
15960
16842
  keyPressHandler;
15961
16843
  constructor(params) {
@@ -16029,6 +16911,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16029
16911
  linkPreviousTextDecoration
16030
16912
  }
16031
16913
  });
16914
+ text.setIndentStep(this.indentStep());
16915
+ text.setLinkStyle({
16916
+ color: this.config.link.defaultColor,
16917
+ hoverColor: this.config.link.hoverColor
16918
+ });
16032
16919
  this.setupDefaultNodeAugmentation(text);
16033
16920
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16034
16921
  text.getTransformerProperties = function() {
@@ -16124,7 +17011,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16124
17011
  this.applySize(shape);
16125
17012
  },
16126
17013
  getLayout: () => shape.ensureLayout(),
16127
- measure: (t) => shape.measureText(t),
17014
+ measure: (t, f) => shape.measureText(t, f),
16128
17015
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16129
17016
  getContentOffset: () => shape.getContentOffset(),
16130
17017
  getSize: () => ({
@@ -16175,7 +17062,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16175
17062
  y: p.y * sy
16176
17063
  };
16177
17064
  },
17065
+ revealCaret: (p) => {
17066
+ const box = container;
17067
+ const w = box.clientWidth;
17068
+ const h = box.clientHeight;
17069
+ if (!w || !h) return;
17070
+ const f = this.config.edition.followPadding;
17071
+ const dx = p.x < f.left ? f.left - p.x : p.x > w - f.right ? w - f.right - p.x : 0;
17072
+ const dy = p.y < f.top ? f.top - p.y : p.y > h - f.bottom ? h - f.bottom - p.y : 0;
17073
+ if (!dx && !dy) return;
17074
+ stage.x(stage.x() + dx);
17075
+ stage.y(stage.y() + dy);
17076
+ this.instance.emitEvent("onStageMove");
17077
+ },
16178
17078
  selectAllOnStart: !create,
17079
+ richText: this.isRich(shape),
17080
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17081
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16179
17082
  caretColor: this.config.cursor.color,
16180
17083
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16181
17084
  ...this.config.edition.outline.enabled && {
@@ -16211,6 +17114,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16211
17114
  exitEditMode(shape) {
16212
17115
  const session = this.session;
16213
17116
  if (!session || !this.editing) return;
17117
+ clearTimeout(this.selectionTimer);
16214
17118
  const { changed } = session.getResult();
16215
17119
  session.destroy();
16216
17120
  this.session = null;
@@ -16273,11 +17177,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16273
17177
  text.getLayer()?.batchDraw();
16274
17178
  }
16275
17179
  const layout = text.getAttrs().layout;
16276
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16277
- text.width(text.width() * text.scaleX());
16278
- resetScale(text);
16279
- text.fontSize(text.fontSize() * text.scaleY());
16280
- }
17180
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16281
17181
  text.setAttr("shouldUpdateOnTransform", false);
16282
17182
  text.getLayer()?.batchDraw();
16283
17183
  });
@@ -16321,6 +17221,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16321
17221
  };
16322
17222
  const isLinked = Boolean(nextProps.link);
16323
17223
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17224
+ shape.setIndentStep(this.indentStep());
16324
17225
  shape.setAttrs({
16325
17226
  ...nextProps,
16326
17227
  ...this.outlineProps(),
@@ -16358,7 +17259,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16358
17259
  delete cleanedAttrs.dragBoundFunc;
16359
17260
  if (cleanedAttrs.link) {
16360
17261
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16361
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17262
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16362
17263
  }
16363
17264
  return {
16364
17265
  key: attrs.id ?? "",
@@ -16373,18 +17274,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16373
17274
  }
16374
17275
  };
16375
17276
  }
17277
+ linkCtxMenuGuardRegistered = false;
17278
+ linkHoverText = null;
17279
+ linkModifier = false;
17280
+ linkHoverRefresh = null;
17281
+ linkHitAt(text) {
17282
+ const link = text.getAttrs().link;
17283
+ if (link) {
17284
+ const p = this.localPointer(text);
17285
+ return p.x >= 0 && p.y >= 0 && p.x <= text.width() && p.y <= text.height() ? { url: link } : null;
17286
+ }
17287
+ return text.linkAtPoint(this.localPointer(text));
17288
+ }
17289
+ registerLinkContextMenuGuard() {
17290
+ if (this.linkCtxMenuGuardRegistered || this.instance.isServerSide()) return;
17291
+ this.linkCtxMenuGuardRegistered = true;
17292
+ const stage = this.instance.getStage();
17293
+ const signal = this.instance.getEventsController().signal;
17294
+ const syncModifier = (e) => {
17295
+ const next = e.ctrlKey || e.metaKey;
17296
+ if (next === this.linkModifier) return;
17297
+ this.linkModifier = next;
17298
+ this.linkHoverRefresh?.();
17299
+ };
17300
+ window.addEventListener("keydown", syncModifier, { signal });
17301
+ window.addEventListener("keyup", syncModifier, { signal });
17302
+ window.addEventListener("blur", () => {
17303
+ this.linkModifier = false;
17304
+ this.linkHoverRefresh?.();
17305
+ }, { signal });
17306
+ stage.container().addEventListener("contextmenu", (e) => {
17307
+ if (this.editing || !e.ctrlKey) return;
17308
+ const text = this.linkHoverText;
17309
+ if (!text) return;
17310
+ stage.setPointersPositions(e);
17311
+ const hit = this.linkHitAt(text);
17312
+ if (!hit) return;
17313
+ e.preventDefault();
17314
+ e.stopPropagation();
17315
+ this.openUrl(hit.url);
17316
+ }, {
17317
+ capture: true,
17318
+ signal: this.instance.getEventsController().signal
17319
+ });
17320
+ }
16376
17321
  setupLinkBehavior(text) {
16377
- text.handleMouseover = () => {
16378
- if (!text.getAttrs().link) return;
16379
- text.fill(this.config.link.hoverColor);
16380
- text.getLayer()?.batchDraw();
17322
+ this.registerLinkContextMenuGuard();
17323
+ const container = () => this.instance.getStage().container();
17324
+ const idleCursor = () => this.isNodeSelected(text) ? "grab" : "pointer";
17325
+ const applyHover = () => {
17326
+ if (this.editing) return;
17327
+ this.linkHoverText = text;
17328
+ const hit = this.linkModifier ? this.linkHitAt(text) : null;
17329
+ if (!text.getAttrs().link) text.setHoveredRun(hit?.key ?? null);
17330
+ else {
17331
+ text.fill(hit ? this.config.link.hoverColor : this.config.link.defaultColor);
17332
+ text.getLayer()?.batchDraw();
17333
+ }
17334
+ container().style.cursor = hit ? "pointer" : idleCursor();
17335
+ };
17336
+ const updateHover = (e) => {
17337
+ if (e?.evt) this.linkModifier = e.evt.ctrlKey || e.evt.metaKey;
17338
+ this.linkHoverRefresh = applyHover;
17339
+ applyHover();
17340
+ };
17341
+ text.handleMouseover = (e) => {
17342
+ updateHover(e);
16381
17343
  };
16382
17344
  text.handleMouseout = () => {
17345
+ if (this.linkHoverText === text) {
17346
+ this.linkHoverText = null;
17347
+ this.linkHoverRefresh = null;
17348
+ }
17349
+ text.setHoveredRun(null);
16383
17350
  if (!text.getAttrs().link) return;
16384
17351
  text.fill(this.config.link.defaultColor);
16385
17352
  text.getLayer()?.batchDraw();
16386
17353
  };
16387
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17354
+ text.on("pointermove", updateHover);
17355
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16388
17356
  let linkClickTimeout;
16389
17357
  const clearLinkClickTimeout = () => {
16390
17358
  if (linkClickTimeout) {
@@ -16398,27 +17366,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16398
17366
  previousDblClick();
16399
17367
  };
16400
17368
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16401
- if (!text.getAttrs().link) return;
17369
+ const runLink = text.getAttrs().link ? null : text.linkAtPoint(this.localPointer(text));
17370
+ if (!text.getAttrs().link && !runLink) return;
17371
+ const open = () => runLink ? this.openUrl(runLink.url) : this.openTextLink(text);
16402
17372
  if (ctrlOrMetaPressed) {
16403
17373
  clearLinkClickTimeout();
16404
- this.openTextLink(text);
17374
+ open();
16405
17375
  return;
16406
17376
  }
16407
17377
  if (wasSelected) {
16408
17378
  clearLinkClickTimeout();
16409
17379
  linkClickTimeout = setTimeout(() => {
16410
17380
  linkClickTimeout = void 0;
16411
- this.openTextLink(text);
17381
+ open();
16412
17382
  }, Konva.dblClickWindow);
16413
17383
  }
16414
17384
  };
16415
17385
  }
16416
- openTextLink(textNode) {
17386
+ localPointer(text) {
17387
+ return text.getRelativePointerPosition() ?? {
17388
+ x: -1,
17389
+ y: -1
17390
+ };
17391
+ }
17392
+ lastLinkOpen = 0;
17393
+ openUrl(link) {
16417
17394
  if (this.instance.isServerSide()) return;
16418
- const link = textNode.getAttrs().link;
16419
17395
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17396
+ const now = Date.now();
17397
+ if (now - this.lastLinkOpen < 500) return;
17398
+ this.lastLinkOpen = now;
16420
17399
  window.open(link, "_blank", "noopener,noreferrer");
16421
17400
  }
17401
+ openTextLink(textNode) {
17402
+ this.openUrl(textNode.getAttrs().link);
17403
+ }
16422
17404
  getLink(nodeInstance) {
16423
17405
  const link = nodeInstance.getAttrs().link;
16424
17406
  return typeof link === "string" ? link : void 0;
@@ -16438,6 +17420,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16438
17420
  delete serialized.props.linkPreviousTextDecoration;
16439
17421
  this.instance.updateNode(serialized);
16440
17422
  }
17423
+ indentStep() {
17424
+ const step = this.config.richText.indentStep;
17425
+ return Number.isFinite(step) && step > 0 ? step : 1.5;
17426
+ }
17427
+ /**
17428
+ * Rich text is on for a node when its `richText` attribute says so;
17429
+ * nodes without the attribute follow `config.richText.enabled`.
17430
+ */
17431
+ isRich(nodeInstance) {
17432
+ const own = nodeInstance.getAttrs().richText;
17433
+ return typeof own === "boolean" ? own : this.config.richText.enabled;
17434
+ }
17435
+ isRichTextEnabled(nodeInstance) {
17436
+ return nodeInstance ? this.isRich(nodeInstance) : this.config.richText.enabled;
17437
+ }
17438
+ activeSession(shape) {
17439
+ return this.editing && this.editingNodeId === shape.id() ? this.session : null;
17440
+ }
17441
+ emitFormatChange(shape, reason) {
17442
+ this.instance.emitEvent("onTextFormatChange", {
17443
+ node: shape,
17444
+ format: this.getSelectionFormat(shape),
17445
+ reason
17446
+ });
17447
+ }
17448
+ /** Selection events fire often while dragging: coalesce them. */
17449
+ scheduleSelectionEvent(shape) {
17450
+ if (this.selectionTimer) return;
17451
+ this.selectionTimer = setTimeout(() => {
17452
+ this.selectionTimer = void 0;
17453
+ const session = this.activeSession(shape);
17454
+ if (!session) return;
17455
+ const { start, end } = session.getSelection();
17456
+ this.instance.emitEvent("onTextSelectionChange", {
17457
+ node: shape,
17458
+ start,
17459
+ end,
17460
+ format: session.getFormat()
17461
+ });
17462
+ }, 50);
17463
+ }
17464
+ /**
17465
+ * Format of the active selection while editing, otherwise of `range`
17466
+ * (default: the whole text). Sizes are px.
17467
+ */
17468
+ getSelectionFormat(nodeInstance, range) {
17469
+ const shape = nodeInstance;
17470
+ const session = this.activeSession(shape);
17471
+ if (session) return session.getFormat();
17472
+ const blocks = shape.getBlocks();
17473
+ const r = range ?? {
17474
+ start: 0,
17475
+ end: blocksToText(blocks).length
17476
+ };
17477
+ return getSelectionFormat(blocks, r.start, r.end, shape.fontSize());
17478
+ }
17479
+ /**
17480
+ * Run a block transform on a non-editing node: one `updateNode`, so one
17481
+ * Weave undo step. Returns false when rich text is disabled.
17482
+ */
17483
+ commandOnNode(shape, range, fn) {
17484
+ const blocks = shape.getBlocks();
17485
+ const next = fn(blocks, range?.start ?? 0, range?.end ?? blocksToText(blocks).length);
17486
+ if (JSON.stringify(next) === JSON.stringify(blocks)) return false;
17487
+ shape.setBlocks(next);
17488
+ this.applySize(shape);
17489
+ this.instance.updateNode(this.serialize(shape));
17490
+ this.emitFormatChange(shape, "command");
17491
+ return true;
17492
+ }
17493
+ /**
17494
+ * Set a mark (`undefined` clears it). `size` is in px. While the node is
17495
+ * being edited it applies to the editor selection, otherwise to `range`
17496
+ * (default: the whole text).
17497
+ */
17498
+ applyMark(nodeInstance, key, value, range) {
17499
+ const shape = nodeInstance;
17500
+ if (!this.isRich(shape)) return false;
17501
+ let v = value;
17502
+ if (key === "link" && v !== void 0 && !isValidTextLink(String(v))) return false;
17503
+ if (key === "size" && typeof value === "number") v = sizeToRatio(value, shape.fontSize());
17504
+ const session = this.activeSession(shape);
17505
+ if (session) {
17506
+ session.setMark(key, v);
17507
+ return true;
17508
+ }
17509
+ return this.commandOnNode(shape, range, (b, s, e) => applyMark(b, s, e, key, v));
17510
+ }
17511
+ /** Link a range (or the editor selection) to an http(s) URL. */
17512
+ setRunLink(nodeInstance, url, range) {
17513
+ if (typeof url !== "string" || !isValidTextLink(url)) return false;
17514
+ return this.applyMark(nodeInstance, "link", url, range);
17515
+ }
17516
+ removeRunLink(nodeInstance, range) {
17517
+ return this.applyMark(nodeInstance, "link", void 0, range);
17518
+ }
17519
+ toggleMark(nodeInstance, key, range) {
17520
+ const shape = nodeInstance;
17521
+ if (!this.isRich(shape)) return false;
17522
+ const session = this.activeSession(shape);
17523
+ if (session) {
17524
+ session.toggleMark(key);
17525
+ return true;
17526
+ }
17527
+ return this.commandOnNode(shape, range, (b, s, e) => toggleMark(b, s, e, key));
17528
+ }
17529
+ toggleList(nodeInstance, kind, range) {
17530
+ const shape = nodeInstance;
17531
+ if (!this.isRich(shape)) return false;
17532
+ const session = this.activeSession(shape);
17533
+ if (session) {
17534
+ session.toggleList(kind);
17535
+ return true;
17536
+ }
17537
+ return this.commandOnNode(shape, range, (b, s, e) => toggleList(b, s, e, kind));
17538
+ }
17539
+ /** Align every paragraph touched by the range (default: all). */
17540
+ setAlign(nodeInstance, align, range) {
17541
+ const shape = nodeInstance;
17542
+ if (!this.isRich(shape)) return false;
17543
+ if (align !== void 0 && ![
17544
+ "left",
17545
+ "center",
17546
+ "right"
17547
+ ].includes(align)) return false;
17548
+ const session = this.activeSession(shape);
17549
+ if (session) {
17550
+ session.setAlign(align);
17551
+ return true;
17552
+ }
17553
+ return this.commandOnNode(shape, range, (b, s, e) => setBlockAlign(b, s, e, align));
17554
+ }
17555
+ indent(nodeInstance, range) {
17556
+ const shape = nodeInstance;
17557
+ if (!this.isRich(shape)) return false;
17558
+ const session = this.activeSession(shape);
17559
+ if (session) {
17560
+ session.indent();
17561
+ return true;
17562
+ }
17563
+ return this.commandOnNode(shape, range, indentBlocks);
17564
+ }
17565
+ outdent(nodeInstance, range) {
17566
+ const shape = nodeInstance;
17567
+ if (!this.isRich(shape)) return false;
17568
+ const session = this.activeSession(shape);
17569
+ if (session) {
17570
+ session.outdent();
17571
+ return true;
17572
+ }
17573
+ return this.commandOnNode(shape, range, outdentBlocks);
17574
+ }
17575
+ /**
17576
+ * Scale the whole text: sets the node `fontSize` (px); size marks are
17577
+ * ratios of it, so every run scales with it.
17578
+ */
17579
+ setBaseFontSize(nodeInstance, px) {
17580
+ const shape = nodeInstance;
17581
+ if (!this.isRich(shape)) return false;
17582
+ if (!Number.isFinite(px) || px <= 0) return false;
17583
+ if (shape.fontSize() === px) return false;
17584
+ shape.fontSize(px);
17585
+ this.applySize(shape);
17586
+ if (!this.activeSession(shape)) this.instance.updateNode(this.serialize(shape));
17587
+ this.emitFormatChange(shape, "base-size");
17588
+ return true;
17589
+ }
16441
17590
  resetSmartLayout(textNode) {
16442
17591
  textNode.setAttr("smartFixedWidth", void 0);
16443
17592
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16456,7 +17605,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16456
17605
  return next;
16457
17606
  }
16458
17607
  static getSchema() {
16459
- return WeaveTextNode.getSchema();
17608
+ const base = WeaveTextNode.getSchema();
17609
+ const marks = z.object({
17610
+ bold: z.boolean().optional(),
17611
+ italic: z.boolean().optional(),
17612
+ underline: z.boolean().optional(),
17613
+ strike: z.boolean().optional(),
17614
+ color: z.string().optional(),
17615
+ size: z.number().positive().optional().describe("Ratio of the node fontSize (1 is omitted)."),
17616
+ family: z.string().optional(),
17617
+ lineHeight: z.number().positive().optional(),
17618
+ link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional()
17619
+ }).passthrough();
17620
+ const block = z.object({
17621
+ runs: z.array(z.object({
17622
+ t: z.string(),
17623
+ m: marks.optional()
17624
+ })),
17625
+ kind: z.enum(["ul", "ol"]).optional(),
17626
+ depth: z.number().int().min(0).optional(),
17627
+ start: z.number().int().min(1).optional(),
17628
+ align: z.enum([
17629
+ "left",
17630
+ "center",
17631
+ "right"
17632
+ ]).optional()
17633
+ });
17634
+ return base.extend({ props: base.shape.props.extend({
17635
+ richText: z.boolean().optional().describe("Enables rich text commands for this node."),
17636
+ blocks: z.array(block).optional().describe("Rich content: paragraphs of runs with marks and optional list fields. `text` is the derived plain mirror.")
17637
+ }) });
16460
17638
  }
16461
17639
  };
16462
17640
  //#endregion
@@ -26978,8 +28156,16 @@ var WeaveTextToolAction = class extends WeaveAction {
26978
28156
  cancelAction;
26979
28157
  onPropsChange = void 0;
26980
28158
  onInit = void 0;
26981
- constructor() {
28159
+ toolName;
28160
+ initialProps;
28161
+ /**
28162
+ * `name` and `props` let an app register several text tools, e.g. a
28163
+ * plain one and one creating rich text nodes (`props: { richText: true }`).
28164
+ */
28165
+ constructor(params) {
26982
28166
  super();
28167
+ this.toolName = params?.name ?? "textTool";
28168
+ this.initialProps = params?.props ?? {};
26983
28169
  this.initialize();
26984
28170
  }
26985
28171
  initialize() {
@@ -26991,7 +28177,7 @@ var WeaveTextToolAction = class extends WeaveAction {
26991
28177
  this.props = this.initProps();
26992
28178
  }
26993
28179
  getName() {
26994
- return TEXT_TOOL_ACTION_NAME;
28180
+ return this.toolName;
26995
28181
  }
26996
28182
  initProps() {
26997
28183
  return {
@@ -27003,7 +28189,8 @@ var WeaveTextToolAction = class extends WeaveAction {
27003
28189
  align: "left",
27004
28190
  lineHeight: 1,
27005
28191
  verticalAlign: "top",
27006
- strokeEnabled: false
28192
+ strokeEnabled: false,
28193
+ ...this.initialProps
27007
28194
  };
27008
28195
  }
27009
28196
  setupEvents() {
@@ -35114,15 +36301,12 @@ var WeaveNodesSnappingPlugin = class extends WeavePlugin {
35114
36301
  register(instance) {
35115
36302
  super.register(instance);
35116
36303
  const hooks = this.instance.getHooks();
35117
- hooks.hook("weave:onNodeDragStart", ({ node }) => {
35118
- let containerId = "";
35119
- if (node.getAttrs().containerId) containerId = node.id();
35120
- if (node && this.snappingManagerCustomGuides.isCustomGuidesVisible(containerId)) this.snappingManagerCustomGuides.hideCustomGuides(node.id());
35121
- });
35122
- hooks.hook("weave:onNodeDragEnd", ({ node }) => {
35123
- let containerId = "";
35124
- if (node.getAttrs().containerId) containerId = node.id();
35125
- if (node && this.snappingManagerCustomGuides.isCustomGuidesVisible(containerId)) this.snappingManagerCustomGuides.renderCustomGuides(node.id());
36304
+ const guidedContainers = (nodes) => nodes.filter((node) => node.getAttrs().containerId && this.snappingManagerCustomGuides.isCustomGuidesVisible(node.id()));
36305
+ hooks.hook("weave:onTransformerDragStart", ({ nodes }) => {
36306
+ for (const node of guidedContainers(nodes)) this.snappingManagerCustomGuides.hideCustomGuides(node.id());
36307
+ });
36308
+ hooks.hook("weave:onTransformerDragEnd", ({ nodes }) => {
36309
+ for (const node of guidedContainers(nodes)) this.snappingManagerCustomGuides.renderCustomGuides(node.id());
35126
36310
  });
35127
36311
  return this;
35128
36312
  }