@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/types.js CHANGED
@@ -4771,12 +4771,39 @@ function registerKeyboardHandlers(ctx) {
4771
4771
  }
4772
4772
  }
4773
4773
  if (e.code === "Backspace" || e.code === "Delete") {
4774
- Promise.resolve().then(() => {
4774
+ queueMicrotask(() => {
4775
4775
  ctx.removeSelectedNodes();
4776
4776
  });
4777
4777
  return;
4778
4778
  }
4779
4779
  }, { signal });
4780
+ const isModifier = (e) => e.key === "Control" || e.key === "Meta";
4781
+ const hideHover = () => ctx.getTransformerController().getHoverTransformer().nodes([]);
4782
+ const restoreHover = () => {
4783
+ const pos = stage.getPointerPosition();
4784
+ const shape = pos ? stage.getIntersection(pos) : null;
4785
+ if (!shape) return;
4786
+ shape.fire("pointerover", {
4787
+ evt: {
4788
+ ctrlKey: false,
4789
+ metaKey: false
4790
+ },
4791
+ target: shape
4792
+ }, true);
4793
+ };
4794
+ window.addEventListener("keydown", (e) => {
4795
+ if (isModifier(e)) hideHover();
4796
+ }, {
4797
+ signal,
4798
+ capture: true
4799
+ });
4800
+ window.addEventListener("keyup", (e) => {
4801
+ if (isModifier(e)) restoreHover();
4802
+ }, {
4803
+ signal,
4804
+ capture: true
4805
+ });
4806
+ window.addEventListener("blur", restoreHover, { signal });
4780
4807
  stage.container().addEventListener("keyup", (e) => {
4781
4808
  if (e.code === "Space") ctx.setSpaceKeyPressed(false);
4782
4809
  }, { signal });
@@ -6532,7 +6559,10 @@ var WeaveNode = class {
6532
6559
  }
6533
6560
  handleMouseOver(e, node) {
6534
6561
  const stage = this.instance.getStage();
6535
- if (e.evt.ctrlKey || e.evt.metaKey) return false;
6562
+ if (e.evt.ctrlKey || e.evt.metaKey) {
6563
+ this.hideHoverState();
6564
+ return false;
6565
+ }
6536
6566
  const user = this.instance.getStore().getUser();
6537
6567
  const activeAction = this.instance.getActiveAction();
6538
6568
  const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled();
@@ -6579,7 +6609,10 @@ var WeaveNode = class {
6579
6609
  return cancelBubble;
6580
6610
  }
6581
6611
  handleMouseout(e, node) {
6582
- if (e.evt?.ctrlKey || e.evt?.metaKey) return false;
6612
+ if (e.evt?.ctrlKey || e.evt?.metaKey) {
6613
+ this.hideHoverState();
6614
+ return false;
6615
+ }
6583
6616
  if (this.instance.getInstanceRecursive(node)) this.hideHoverState();
6584
6617
  }
6585
6618
  create(key, props) {
@@ -9502,7 +9535,7 @@ var WeaveRegisterManager = class {
9502
9535
  };
9503
9536
  //#endregion
9504
9537
  //#region package.json
9505
- var version = "5.7.0-SNAPSHOT.597.1";
9538
+ var version = "5.7.0-SNAPSHOT.600.1";
9506
9539
  //#endregion
9507
9540
  //#region src/managers/setup.ts
9508
9541
  var WeaveSetupManager = class {
@@ -13804,7 +13837,7 @@ var WeaveTextNode = class extends WeaveNode {
13804
13837
  delete cleanedAttrs.dragBoundFunc;
13805
13838
  if (cleanedAttrs.link) {
13806
13839
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
13807
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
13840
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
13808
13841
  }
13809
13842
  return {
13810
13843
  key: attrs.id ?? "",
@@ -14440,6 +14473,31 @@ var WeaveTextNode = class extends WeaveNode {
14440
14473
  };
14441
14474
  //#endregion
14442
14475
  //#region src/nodes/text-v2/model.ts
14476
+ const MIN_LINE_HEIGHT = .5;
14477
+ /** Valid `lineHeight` mark of a run, or undefined (use the node value). */
14478
+ function runLineHeight(m) {
14479
+ const v = m?.lineHeight;
14480
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(4, Math.max(MIN_LINE_HEIGHT, v)) : void 0;
14481
+ }
14482
+ /** Only absolute http(s) URLs are links (same rule as the legacy node). */
14483
+ function isSafeLink(value) {
14484
+ if (typeof value !== "string") return false;
14485
+ try {
14486
+ return ["http:", "https:"].includes(new URL(value).protocol);
14487
+ } catch {
14488
+ return false;
14489
+ }
14490
+ }
14491
+ /** Block fields that describe structure (not content). */
14492
+ function blockFields(block, withStart) {
14493
+ const out = {};
14494
+ if (!block) return out;
14495
+ if (block.kind) out.kind = block.kind;
14496
+ if (block.depth) out.depth = block.depth;
14497
+ if (block.align) out.align = block.align;
14498
+ if (withStart && block.start !== void 0) out.start = block.start;
14499
+ return out;
14500
+ }
14443
14501
  function isEmptyMarks(m) {
14444
14502
  return !m || Object.keys(m).length === 0;
14445
14503
  }
@@ -14520,7 +14578,7 @@ function marksAt(runs, offset) {
14520
14578
  * blocks) with plain `insert`. Marks on untouched text are preserved; the
14521
14579
  * inserted text inherits marks of the character before `start`.
14522
14580
  */
14523
- function replaceRange(blocks, start, end, insert) {
14581
+ function replaceRange(blocks, start, end, insert, insertMarks) {
14524
14582
  const total = blocksToText(blocks).length;
14525
14583
  start = Math.max(0, Math.min(start, total));
14526
14584
  end = Math.max(start, Math.min(end, total));
@@ -14529,6 +14587,7 @@ function replaceRange(blocks, start, end, insert) {
14529
14587
  const prefixBlocks = [];
14530
14588
  const suffixBlocks = [];
14531
14589
  let inheritedMarks;
14590
+ let startBlock;
14532
14591
  let pos = 0;
14533
14592
  blocks.forEach((block) => {
14534
14593
  const len = blockText(block).length;
@@ -14541,11 +14600,13 @@ function replaceRange(blocks, start, end, insert) {
14541
14600
  if (start >= bStart && start <= bEnd) {
14542
14601
  const local = start - bStart;
14543
14602
  before = sliceRuns(block.runs, 0, local);
14603
+ startBlock = block;
14544
14604
  inheritedMarks = marksAt(block.runs, local);
14545
14605
  }
14546
14606
  if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
14547
14607
  }
14548
14608
  });
14609
+ if (insertMarks !== void 0) inheritedMarks = insertMarks ?? void 0;
14549
14610
  const lines = insert.split("\n");
14550
14611
  const mk = (t) => t === "" ? [] : [{
14551
14612
  t,
@@ -14556,7 +14617,10 @@ function replaceRange(blocks, start, end, insert) {
14556
14617
  let runs = mk(line);
14557
14618
  if (idx === 0) runs = [...before, ...runs];
14558
14619
  if (idx === lines.length - 1) runs = [...runs, ...after];
14559
- middle.push({ runs: normalizeRuns(runs) });
14620
+ middle.push({
14621
+ ...blockFields(startBlock, idx === 0),
14622
+ runs: normalizeRuns(runs)
14623
+ });
14560
14624
  });
14561
14625
  return [
14562
14626
  ...prefixBlocks,
@@ -14564,8 +14628,331 @@ function replaceRange(blocks, start, end, insert) {
14564
14628
  ...suffixBlocks
14565
14629
  ];
14566
14630
  }
14631
+ function safeBase(base) {
14632
+ return Number.isFinite(base) && base > 0 ? base : 1;
14633
+ }
14634
+ /** px -> stored ratio (4 decimals). */
14635
+ function sizeToRatio(px, base) {
14636
+ return Math.round(px / safeBase(base) * 1e4) / 1e4;
14637
+ }
14638
+ /** stored ratio -> px (rounded to 0.1). Invalid ratios resolve to the base. */
14639
+ function ratioToSize(ratio, base) {
14640
+ const b = safeBase(base);
14641
+ return Math.round((typeof ratio === "number" && ratio > 0 ? ratio : 1) * b * 10) / 10;
14642
+ }
14643
+ /** Effective ratio of a run (invalid or missing -> 1). */
14644
+ function runRatio(m) {
14645
+ const r = m?.size;
14646
+ return typeof r === "number" && Number.isFinite(r) && r > 0 ? r : 1;
14647
+ }
14648
+ function normalizeMarks(m) {
14649
+ const out = {};
14650
+ for (const [k, v] of Object.entries(m)) {
14651
+ if (v === void 0 || v === false || v === null || v === "") continue;
14652
+ if (k === "size" && v === 1) continue;
14653
+ out[k] = v;
14654
+ }
14655
+ return Object.keys(out).length === 0 ? void 0 : out;
14656
+ }
14657
+ /** Normalized marks of a run, or undefined when empty. */
14658
+ function cleanMarks(m) {
14659
+ return m ? normalizeMarks(m) : void 0;
14660
+ }
14661
+ function blockSpans(blocks) {
14662
+ const out = [];
14663
+ let pos = 0;
14664
+ blocks.forEach((b, index) => {
14665
+ const len = blockText(b).length;
14666
+ out.push({
14667
+ index,
14668
+ start: pos,
14669
+ end: pos + len
14670
+ });
14671
+ pos += len + 1;
14672
+ });
14673
+ return out;
14674
+ }
14675
+ /** Indices of blocks touched by [start, end] (collapsed range -> its block). */
14676
+ function blockIndicesInRange(blocks, start, end) {
14677
+ const out = [];
14678
+ for (const s of blockSpans(blocks)) if (s.end >= start && s.start <= end) {
14679
+ if (end > start && s.start === end && s.start !== start) continue;
14680
+ out.push(s.index);
14681
+ }
14682
+ return out;
14683
+ }
14684
+ /** Marks that typing at a collapsed offset would inherit. */
14685
+ function marksAtOffset(blocks, offset) {
14686
+ for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) return cleanMarks(marksAt(blocks[s.index].runs, offset - s.start));
14687
+ }
14688
+ function mapRunsInRange(block, from, to, fn) {
14689
+ const runs = [];
14690
+ let pos = 0;
14691
+ for (const run of block.runs) {
14692
+ const rs = pos;
14693
+ const re = pos + run.t.length;
14694
+ pos = re;
14695
+ const s = Math.max(from, rs);
14696
+ const e = Math.min(to, re);
14697
+ if (s >= e) {
14698
+ runs.push(run);
14699
+ continue;
14700
+ }
14701
+ if (s > rs) runs.push({
14702
+ t: run.t.slice(0, s - rs),
14703
+ m: run.m
14704
+ });
14705
+ runs.push({
14706
+ t: run.t.slice(s - rs, e - rs),
14707
+ m: fn(run.m)
14708
+ });
14709
+ if (e < re) runs.push({
14710
+ t: run.t.slice(e - rs),
14711
+ m: run.m
14712
+ });
14713
+ }
14714
+ return {
14715
+ ...block,
14716
+ runs: normalizeRuns(runs)
14717
+ };
14718
+ }
14719
+ /** Set (or remove with `undefined`) one mark over the global range. */
14720
+ function applyMark(blocks, start, end, key, value) {
14721
+ if (start >= end) return blocks;
14722
+ if (key === "lineHeight" && value !== void 0) {
14723
+ const lh = runLineHeight({ lineHeight: value });
14724
+ if (lh === void 0) return blocks;
14725
+ value = lh;
14726
+ }
14727
+ const spans = blockSpans(blocks);
14728
+ return blocks.map((block, i) => {
14729
+ const s = spans[i];
14730
+ if (s.end <= start || s.start >= end) return block;
14731
+ return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
14732
+ ...m,
14733
+ [key]: value
14734
+ }));
14735
+ });
14736
+ }
14737
+ /** Query one mark over a range; collapsed ranges report the caret marks. */
14738
+ function queryMark(blocks, start, end, key, stored) {
14739
+ const values = [];
14740
+ if (start >= end) {
14741
+ const v = (stored ?? marksAtOffset(blocks, start))?.[key];
14742
+ return v === void 0 || v === false ? { state: "none" } : {
14743
+ state: "all",
14744
+ value: v
14745
+ };
14746
+ }
14747
+ const spans = blockSpans(blocks);
14748
+ blocks.forEach((block, i) => {
14749
+ const s = spans[i];
14750
+ if (s.end <= start || s.start >= end) return;
14751
+ const from = Math.max(start, s.start) - s.start;
14752
+ const to = Math.min(end, s.end) - s.start;
14753
+ for (const r of sliceRuns(block.runs, from, to)) {
14754
+ const v = r.m?.[key];
14755
+ values.push(v === false ? void 0 : v);
14756
+ }
14757
+ });
14758
+ if (values.length === 0) return { state: "none" };
14759
+ const set = values.filter((v) => v !== void 0);
14760
+ if (set.length === 0) return { state: "none" };
14761
+ if (set.length < values.length) return { state: "some" };
14762
+ return values.every((v) => v === values[0]) ? {
14763
+ state: "all",
14764
+ value: values[0]
14765
+ } : { state: "some" };
14766
+ }
14767
+ /** Toggle a boolean mark: removes when fully set, sets otherwise. */
14768
+ function toggleMark(blocks, start, end, key) {
14769
+ return applyMark(blocks, start, end, key, queryMark(blocks, start, end, key).state === "all" ? void 0 : true);
14770
+ }
14771
+ function mapBlocks(blocks, indices, fn) {
14772
+ const set = new Set(indices);
14773
+ return blocks.map((b, i) => set.has(i) ? fn(b) : b);
14774
+ }
14775
+ function stripList(b) {
14776
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
14777
+ return rest;
14778
+ }
14779
+ /** Toggle list `kind` on the blocks of a range (all already that kind -> off). */
14780
+ function toggleList(blocks, start, end, kind) {
14781
+ const idx = blockIndicesInRange(blocks, start, end);
14782
+ const allSame = idx.every((i) => blocks[i].kind === kind);
14783
+ return mapBlocks(blocks, idx, (b) => allSame ? stripList(b) : {
14784
+ ...b,
14785
+ kind
14786
+ });
14787
+ }
14788
+ /** Set (or clear with `undefined`) the alignment of the blocks in a range. */
14789
+ function setBlockAlign(blocks, start, end, align) {
14790
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14791
+ if (align === void 0) {
14792
+ const { align: _a, ...rest } = b;
14793
+ return rest;
14794
+ }
14795
+ return {
14796
+ ...b,
14797
+ align
14798
+ };
14799
+ });
14800
+ }
14801
+ /** Increase depth of list blocks in the range (non-list blocks untouched). */
14802
+ function indentBlocks(blocks, start, end) {
14803
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => b.kind ? {
14804
+ ...b,
14805
+ depth: (b.depth ?? 0) + 1
14806
+ } : b);
14807
+ }
14808
+ /** Decrease depth; at depth 0 the list marker is removed. */
14809
+ function outdentBlocks(blocks, start, end) {
14810
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14811
+ if (!b.kind) return b;
14812
+ const d = b.depth ?? 0;
14813
+ if (d <= 0) return stripList(b);
14814
+ const next = {
14815
+ ...b,
14816
+ depth: d - 1
14817
+ };
14818
+ if (d - 1 === 0) delete next.depth;
14819
+ return next;
14820
+ });
14821
+ }
14822
+ const BULLETS = [
14823
+ "•",
14824
+ "◦",
14825
+ "▪"
14826
+ ];
14827
+ function alpha(n) {
14828
+ let s = "";
14829
+ let v = n;
14830
+ while (v > 0) {
14831
+ v--;
14832
+ s = String.fromCharCode(97 + v % 26) + s;
14833
+ v = Math.floor(v / 26);
14834
+ }
14835
+ return s;
14836
+ }
14837
+ function roman(n) {
14838
+ const map = [
14839
+ [1e3, "m"],
14840
+ [900, "cm"],
14841
+ [500, "d"],
14842
+ [400, "cd"],
14843
+ [100, "c"],
14844
+ [90, "xc"],
14845
+ [50, "l"],
14846
+ [40, "xl"],
14847
+ [10, "x"],
14848
+ [9, "ix"],
14849
+ [5, "v"],
14850
+ [4, "iv"],
14851
+ [1, "i"]
14852
+ ];
14853
+ let v = n;
14854
+ let s = "";
14855
+ for (const [k, r] of map) while (v >= k) {
14856
+ s += r;
14857
+ v -= k;
14858
+ }
14859
+ return s;
14860
+ }
14861
+ /**
14862
+ * Marker text per block (null for plain paragraphs). Ordered numbering is
14863
+ * continuous across consecutive same-depth items and restarts after a
14864
+ * non-list block, a shallower item or a kind change; `start` seeds a run.
14865
+ * Markers are layout-only: they never enter `text` or offsets.
14866
+ */
14867
+ function listMarkers(blocks) {
14868
+ const stack = [];
14869
+ return blocks.map((b) => {
14870
+ if (!b.kind) {
14871
+ stack.length = 0;
14872
+ return null;
14873
+ }
14874
+ const d = Math.max(0, Math.floor(b.depth ?? 0));
14875
+ stack.length = Math.min(stack.length, d + 1);
14876
+ while (stack.length < d) stack.push({
14877
+ kind: b.kind,
14878
+ n: 0
14879
+ });
14880
+ const cur = stack[d];
14881
+ if (!cur || cur.kind !== b.kind) stack[d] = {
14882
+ kind: b.kind,
14883
+ n: b.kind === "ol" ? Math.max(1, b.start ?? 1) : 0
14884
+ };
14885
+ else if (b.kind === "ol") cur.n += 1;
14886
+ if (b.kind === "ul") return BULLETS[d % 3];
14887
+ const n = stack[d].n;
14888
+ const style = d % 3;
14889
+ return `${style === 0 ? n : style === 1 ? alpha(n) : roman(n)}.`;
14890
+ });
14891
+ }
14892
+ /**
14893
+ * Format of the range for toolbars. A collapsed range reports the stored
14894
+ * marks (when given) or the marks typing would inherit.
14895
+ */
14896
+ function getSelectionFormat(blocks, start, end, baseFontSize, stored) {
14897
+ const q = (key) => queryMark(blocks, start, end, key, stored);
14898
+ const ratios = [];
14899
+ if (start >= end) ratios.push(runRatio(stored ?? marksAtOffset(blocks, start)));
14900
+ else {
14901
+ const spans = blockSpans(blocks);
14902
+ blocks.forEach((block, i) => {
14903
+ const s = spans[i];
14904
+ if (s.end <= start || s.start >= end) return;
14905
+ const from = Math.max(start, s.start) - s.start;
14906
+ const to = Math.min(end, s.end) - s.start;
14907
+ for (const r of sliceRuns(block.runs, from, to)) ratios.push(runRatio(r.m));
14908
+ });
14909
+ }
14910
+ let size;
14911
+ if (ratios.length === 0 || ratios.every((r) => r === ratios[0])) {
14912
+ const r = ratios[0] ?? 1;
14913
+ size = {
14914
+ state: r === 1 ? "none" : "all",
14915
+ value: ratioToSize(r, baseFontSize)
14916
+ };
14917
+ } else size = { state: "some" };
14918
+ const idx = blockIndicesInRange(blocks, start, end);
14919
+ const kinds = new Set(idx.map((i) => blocks[i].kind ?? "none"));
14920
+ let list = { kind: "none" };
14921
+ if (kinds.size === 1) {
14922
+ const kind = [...kinds][0];
14923
+ const depths = new Set(idx.map((i) => blocks[i].depth ?? 0));
14924
+ list = kind === "none" ? { kind } : {
14925
+ kind,
14926
+ ...depths.size === 1 ? { depth: [...depths][0] } : {}
14927
+ };
14928
+ } else if (kinds.size > 1) list = { kind: "mixed" };
14929
+ return {
14930
+ bold: q("bold"),
14931
+ italic: q("italic"),
14932
+ underline: q("underline"),
14933
+ strike: q("strike"),
14934
+ color: q("color"),
14935
+ family: q("family"),
14936
+ link: q("link"),
14937
+ lineHeight: q("lineHeight"),
14938
+ align: alignQuery(blocks, start, end),
14939
+ size,
14940
+ list
14941
+ };
14942
+ }
14943
+ function alignQuery(blocks, start, end) {
14944
+ const vals = blockIndicesInRange(blocks, start, end).map((i) => blocks[i].align);
14945
+ const set = vals.filter((v) => v !== void 0);
14946
+ if (set.length === 0) return { state: "none" };
14947
+ if (set.length < vals.length || set.some((v) => v !== set[0])) return { state: "some" };
14948
+ return {
14949
+ state: "all",
14950
+ value: set[0]
14951
+ };
14952
+ }
14567
14953
  //#endregion
14568
14954
  //#region src/nodes/text-v2/layout/engine.ts
14955
+ const DEFAULT_INDENT_STEP = 1.5;
14569
14956
  /** Quote multi-word families like `Konva` does so the font shorthand parses. */
14570
14957
  function normalizeFontFamily(fontFamily) {
14571
14958
  return fontFamily.split(",").map((family) => {
@@ -14576,7 +14963,51 @@ function normalizeFontFamily(fontFamily) {
14576
14963
  }).join(", ");
14577
14964
  }
14578
14965
  function fontString(s) {
14579
- return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14966
+ return `${[...`${s.fontStyle} ${s.fontVariant}`.split(/\s+/).filter((token) => token !== "" && token !== "normal"), `${s.fontSize}px`].join(" ")} ${normalizeFontFamily(s.fontFamily)}`;
14967
+ }
14968
+ /** Style of a run: node style overridden by the run marks. */
14969
+ function runStyle(style, m) {
14970
+ if (!m) return style;
14971
+ let fontStyle = style.fontStyle;
14972
+ if (m.bold || m.italic) {
14973
+ const tokens = new Set(fontStyle.split(/\s+/).filter(Boolean));
14974
+ tokens.delete("normal");
14975
+ if (m.italic) tokens.add("italic");
14976
+ if (m.bold) tokens.add("bold");
14977
+ fontStyle = tokens.size === 0 ? "normal" : Array.from(tokens).join(" ");
14978
+ }
14979
+ return {
14980
+ ...style,
14981
+ fontStyle,
14982
+ fontSize: style.fontSize * runRatio(m),
14983
+ fontFamily: typeof m.family === "string" && m.family ? m.family : style.fontFamily
14984
+ };
14985
+ }
14986
+ /** Pure fallback (same constants as Konva.Text when the browser has none). */
14987
+ const approxMetrics = (_font, size) => ({
14988
+ ascent: .91 * size,
14989
+ descent: .21 * size
14990
+ });
14991
+ const metricsCache = /* @__PURE__ */ new Map();
14992
+ /** Ascent/descent used for baseline placement (parity with Konva.Text). */
14993
+ function fontMetrics(font, size) {
14994
+ if (Konva.legacyTextRendering) return {
14995
+ ascent: 0,
14996
+ descent: 0
14997
+ };
14998
+ const hit = metricsCache.get(font);
14999
+ if (hit) return hit;
15000
+ const ctx = Konva.Util.createCanvasElement().getContext("2d");
15001
+ ctx.font = font;
15002
+ const m = ctx.measureText("M");
15003
+ const scale = size / 100;
15004
+ const out = {
15005
+ ascent: m.fontBoundingBoxAscent ?? 91 * scale,
15006
+ descent: m.fontBoundingBoxDescent ?? 21 * scale
15007
+ };
15008
+ if (metricsCache.size > 2e3) metricsCache.clear();
15009
+ metricsCache.set(font, out);
15010
+ return out;
14580
15011
  }
14581
15012
  let sharedContext = null;
14582
15013
  /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
@@ -14625,11 +15056,15 @@ function wordWrapIndex(graphemes, start, matchLen, matchWidth, maxWidth) {
14625
15056
  const seg = graphemes.slice(start, start + matchLen);
14626
15057
  return Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
14627
15058
  }
14628
- function applyAlign(lines, align, width, padding) {
14629
- if (align === "left") return;
15059
+ function applyAlign(lines, width, padding) {
14630
15060
  for (const line of lines) {
14631
- const free = width - line.width - padding * 2;
14632
- line.offsetX = align === "center" ? free / 2 : free;
15061
+ const align = line.align;
15062
+ if (align === "left") {
15063
+ line.offsetX = line.indent;
15064
+ continue;
15065
+ }
15066
+ const free = width - line.indent - line.width - padding * 2;
15067
+ line.offsetX = line.indent + (align === "center" ? free / 2 : free);
14633
15068
  }
14634
15069
  }
14635
15070
  var TextLayoutEngine = class {
@@ -14639,8 +15074,10 @@ var TextLayoutEngine = class {
14639
15074
  /** Entries by signature, so paragraphs that shifted index are reused. */
14640
15075
  pool = /* @__PURE__ */ new Map();
14641
15076
  lastParagraphsRelaid = 0;
14642
- constructor(measurer = canvasMeasurer) {
15077
+ metrics;
15078
+ constructor(measurer = canvasMeasurer, metrics = approxMetrics) {
14643
15079
  this.measure = measurer;
15080
+ this.metrics = metrics;
14644
15081
  }
14645
15082
  /** Number of paragraphs recomputed by the last `layout()` call. */
14646
15083
  getLastRelaidCount() {
@@ -14651,10 +15088,17 @@ var TextLayoutEngine = class {
14651
15088
  this.paragraphs = [];
14652
15089
  this.pool.clear();
14653
15090
  }
14654
- paragraphSig(block, style, bounds) {
14655
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14656
- const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14657
- return `${fontString(style)}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}|${JSON.stringify(block.runs)}`;
15091
+ /** Text indent of a block in px (0 for plain paragraphs). */
15092
+ indentFor(block, style, fixedWidth, maxWidth) {
15093
+ if (!block.kind) return 0;
15094
+ const stepPx = (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15095
+ const wanted = (Math.max(0, Math.floor(block.depth ?? 0)) + 1) * stepPx;
15096
+ if (!fixedWidth) return wanted;
15097
+ const limit = Math.max(0, maxWidth - 4 * style.fontSize);
15098
+ return Math.min(wanted, limit);
15099
+ }
15100
+ sigFor(block, style, fixedWidth, maxWidth, indent) {
15101
+ return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
14658
15102
  }
14659
15103
  /**
14660
15104
  * Pre-compute paragraph layouts for `blocks` within a time budget so a
@@ -14662,14 +15106,14 @@ var TextLayoutEngine = class {
14662
15106
  * Lets callers time-slice very large content (e.g. bulk paste).
14663
15107
  */
14664
15108
  warm(blocks, style, bounds, budgetMs) {
14665
- const font = fontString(style);
14666
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
15109
+ const fixedWidth = isNum(bounds.width);
14667
15110
  const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14668
15111
  const deadline = now() + budgetMs;
14669
15112
  for (const block of blocks) {
14670
- const sig = this.paragraphSig(block, style, bounds);
15113
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15114
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14671
15115
  if (this.pool.has(sig)) continue;
14672
- this.pool.set(sig, this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth));
15116
+ this.pool.set(sig, this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent));
14673
15117
  if (now() >= deadline) return false;
14674
15118
  }
14675
15119
  return true;
@@ -14687,35 +15131,38 @@ var TextLayoutEngine = class {
14687
15131
  return w + text.length * spacing;
14688
15132
  }
14689
15133
  layout(blocks, style, bounds) {
14690
- const font = fontString(style);
14691
15134
  const padding = bounds.padding;
14692
15135
  const fixedWidth = isNum(bounds.width);
14693
15136
  const fixedHeight = isNum(bounds.height);
14694
15137
  const maxWidth = (bounds.width ?? 0) - padding * 2;
14695
15138
  const maxHeightPx = (bounds.height ?? 0) - padding * 2;
14696
15139
  const lineHeightPx = style.lineHeight * style.fontSize;
14697
- const sigBase = `${font}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}`;
15140
+ const markers = listMarkers(blocks);
14698
15141
  const prev = this.paragraphs;
14699
15142
  const next = [];
14700
15143
  let relaid = 0;
14701
15144
  const lines = [];
14702
15145
  let textWidth = 0;
14703
15146
  let offset = 0;
15147
+ let contentHeight = 0;
14704
15148
  for (let p = 0; p < blocks.length; p++) {
14705
15149
  const block = blocks[p];
14706
- const sig = `${sigBase}|${JSON.stringify(block.runs)}`;
15150
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15151
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14707
15152
  let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
14708
15153
  if (!entry) {
14709
- entry = this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth);
15154
+ entry = this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent);
14710
15155
  relaid++;
14711
15156
  }
14712
15157
  next.push(entry);
14713
15158
  const emitted = this.emitLines(entry, block, p, offset, lines, {
14714
- font,
14715
15159
  style,
14716
- lineHeightPx,
15160
+ indent,
15161
+ marker: markers[p],
15162
+ y: contentHeight,
14717
15163
  maxHeightPx: fixedHeight ? maxHeightPx : void 0
14718
15164
  });
15165
+ contentHeight = emitted.y;
14719
15166
  textWidth = Math.max(textWidth, emitted.maxWidth);
14720
15167
  if (emitted.stopped) break;
14721
15168
  offset += blockText(block).length + 1;
@@ -14724,25 +15171,42 @@ var TextLayoutEngine = class {
14724
15171
  this.pool = new Map(next.map((e) => [e.sig, e]));
14725
15172
  this.lastParagraphsRelaid = relaid;
14726
15173
  const width = fixedWidth ? bounds.width : textWidth + padding * 2;
14727
- const height = fixedHeight ? bounds.height : lines.length * lineHeightPx + padding * 2;
14728
- applyAlign(lines, style.align, width, padding);
15174
+ const height = fixedHeight ? bounds.height : contentHeight + padding * 2;
15175
+ applyAlign(lines, width, padding);
14729
15176
  return {
14730
15177
  lines,
14731
15178
  textWidth,
14732
15179
  textHeight: style.fontSize,
14733
15180
  lineHeightPx,
15181
+ contentHeight,
14734
15182
  width,
14735
15183
  height
14736
15184
  };
14737
15185
  }
14738
15186
  /** Append a paragraph's lines to `lines`; stops once a fixed height is full. */
14739
15187
  emitLines(entry, block, paragraph, offset, lines, opts) {
14740
- const { font, style, lineHeightPx, maxHeightPx } = opts;
15188
+ const { style, indent, maxHeightPx } = opts;
15189
+ let { y } = opts;
14741
15190
  let maxWidth = 0;
15191
+ let first = true;
14742
15192
  for (const pl of entry.lines) {
14743
15193
  const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
14744
- const segments = this.buildSegments(entry, block, pl, font, style);
15194
+ const segments = this.buildSegments(entry, block, pl, style);
14745
15195
  const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
15196
+ let marker;
15197
+ if (first && opts.marker) {
15198
+ const m0 = block.runs[0]?.m;
15199
+ const rs = runStyle(style, m0);
15200
+ const stepPx = Math.min(indent, this.stepPx(style));
15201
+ marker = {
15202
+ text: opts.marker,
15203
+ x: Math.max(0, indent - stepPx),
15204
+ font: fontString(rs),
15205
+ fontSize: rs.fontSize,
15206
+ color: typeof m0?.color === "string" ? m0.color : void 0
15207
+ };
15208
+ }
15209
+ first = false;
14746
15210
  lines.push({
14747
15211
  paragraph,
14748
15212
  start: offset + startChar,
@@ -14750,30 +15214,41 @@ var TextLayoutEngine = class {
14750
15214
  text,
14751
15215
  width: segments.reduce((a, s) => a + s.width, 0),
14752
15216
  measuredWidth: pl.measuredWidth,
14753
- offsetX: 0,
14754
- y: lines.length * lineHeightPx,
15217
+ offsetX: indent,
15218
+ y,
15219
+ height: pl.height,
15220
+ baseline: pl.baseline,
15221
+ indent,
15222
+ marker,
15223
+ align: block.align ?? style.align,
14755
15224
  segments,
14756
15225
  lastInParagraph: pl.lastInParagraph
14757
15226
  });
14758
- maxWidth = Math.max(maxWidth, pl.measuredWidth);
14759
- if (maxHeightPx !== void 0 && (lines.length + 1) * lineHeightPx > maxHeightPx) return {
15227
+ y += pl.height;
15228
+ maxWidth = Math.max(maxWidth, pl.measuredWidth + indent);
15229
+ if (maxHeightPx !== void 0 && y + pl.height > maxHeightPx) return {
14760
15230
  stopped: true,
14761
- maxWidth
15231
+ maxWidth,
15232
+ y
14762
15233
  };
14763
15234
  }
14764
15235
  return {
14765
15236
  stopped: false,
14766
- maxWidth
15237
+ maxWidth,
15238
+ y
14767
15239
  };
14768
15240
  }
14769
- rangeWidth(entry, from, to, font, spacing) {
15241
+ stepPx(style) {
15242
+ return (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15243
+ }
15244
+ rangeWidth(entry, from, to, spacing) {
14770
15245
  let width = 0;
14771
15246
  let i = from;
14772
15247
  while (i < to) {
14773
15248
  const run = entry.runOf[i];
14774
15249
  let j = i;
14775
15250
  while (j < to && entry.runOf[j] === run) j++;
14776
- width += this.measureChunk(font, entry.graphemes.slice(i, j).join(""), spacing);
15251
+ width += this.measureChunk(entry.fonts[run], entry.graphemes.slice(i, j).join(""), spacing);
14777
15252
  i = j;
14778
15253
  }
14779
15254
  return width;
@@ -14809,17 +15284,58 @@ var TextLayoutEngine = class {
14809
15284
  matchWidth
14810
15285
  };
14811
15286
  }
14812
- layoutParagraph(block, sig, font, style, fixedWidth, maxWidth) {
15287
+ layoutParagraph(block, sig, style, fixedWidth, maxWidth) {
15288
+ const entry = this.wrapParagraph(block, sig, style, fixedWidth, maxWidth);
15289
+ this.measureLines(entry, style);
15290
+ return entry;
15291
+ }
15292
+ /** Per-line box height and baseline from the runs on each line. */
15293
+ measureLines(entry, style) {
15294
+ const baseFont = fontString(style);
15295
+ for (const line of entry.lines) {
15296
+ let size = 0;
15297
+ let height = 0;
15298
+ let asc = 0;
15299
+ let desc = 0;
15300
+ const seen = /* @__PURE__ */ new Set();
15301
+ for (let g = line.startG; g < line.endG; g++) {
15302
+ const r = entry.runOf[g];
15303
+ if (seen.has(r)) continue;
15304
+ seen.add(r);
15305
+ size = Math.max(size, entry.sizes[r]);
15306
+ height = Math.max(height, entry.lineHeights[r] * entry.sizes[r]);
15307
+ const m = this.metrics(entry.fonts[r], entry.sizes[r]);
15308
+ asc = Math.max(asc, m.ascent);
15309
+ desc = Math.max(desc, m.descent);
15310
+ }
15311
+ if (seen.size === 0) {
15312
+ size = style.fontSize;
15313
+ height = style.lineHeight * size;
15314
+ const m = this.metrics(baseFont, style.fontSize);
15315
+ asc = m.ascent;
15316
+ desc = m.descent;
15317
+ }
15318
+ line.height = height;
15319
+ line.baseline = (asc - desc) / 2 + line.height / 2;
15320
+ }
15321
+ }
15322
+ wrapParagraph(block, sig, style, fixedWidth, maxWidth) {
14813
15323
  const { graphemes, runOf } = flattenBlock(block);
15324
+ const baseFont = fontString(style);
15325
+ const fonts = block.runs.map((r) => r.m ? fontString(runStyle(style, r.m)) : baseFont);
15326
+ const sizes = block.runs.map((r) => style.fontSize * runRatio(r.m));
14814
15327
  const entry = {
14815
15328
  sig,
14816
15329
  graphemes,
14817
15330
  runOf,
15331
+ fonts,
15332
+ lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
15333
+ sizes,
14818
15334
  lines: []
14819
15335
  };
14820
15336
  const n = graphemes.length;
14821
15337
  const spacing = style.letterSpacing;
14822
- const width = (a, b) => this.rangeWidth(entry, a, b, font, spacing);
15338
+ const width = (a, b) => this.rangeWidth(entry, a, b, spacing);
14823
15339
  const shouldWrap = style.wrap !== "none";
14824
15340
  const wrapAtWord = style.wrap !== "char" && shouldWrap;
14825
15341
  const whole = width(0, n);
@@ -14828,7 +15344,9 @@ var TextLayoutEngine = class {
14828
15344
  startG: 0,
14829
15345
  endG: n,
14830
15346
  measuredWidth: whole,
14831
- lastInParagraph: true
15347
+ lastInParagraph: true,
15348
+ height: 0,
15349
+ baseline: 0
14832
15350
  });
14833
15351
  return entry;
14834
15352
  }
@@ -14840,7 +15358,9 @@ var TextLayoutEngine = class {
14840
15358
  startG: start,
14841
15359
  endG: n,
14842
15360
  measuredWidth: matchWidth,
14843
- lastInParagraph: true
15361
+ lastInParagraph: true,
15362
+ height: 0,
15363
+ baseline: 0
14844
15364
  });
14845
15365
  return entry;
14846
15366
  }
@@ -14856,7 +15376,9 @@ var TextLayoutEngine = class {
14856
15376
  startG: start,
14857
15377
  endG: trimTrailingSpace(graphemes, start, end),
14858
15378
  measuredWidth: matchWidth,
14859
- lastInParagraph: false
15379
+ lastInParagraph: false,
15380
+ height: 0,
15381
+ baseline: 0
14860
15382
  });
14861
15383
  start = skipSpace(graphemes, end);
14862
15384
  }
@@ -14864,7 +15386,7 @@ var TextLayoutEngine = class {
14864
15386
  if (last) last.lastInParagraph = true;
14865
15387
  return entry;
14866
15388
  }
14867
- buildSegments(entry, block, pl, font, style) {
15389
+ buildSegments(entry, block, pl, style) {
14868
15390
  const segments = [];
14869
15391
  let x = 0;
14870
15392
  let i = pl.startG;
@@ -14873,12 +15395,16 @@ var TextLayoutEngine = class {
14873
15395
  let j = i;
14874
15396
  while (j < pl.endG && entry.runOf[j] === run) j++;
14875
15397
  const t = entry.graphemes.slice(i, j).join("");
15398
+ const font = entry.fonts[run];
14876
15399
  const width = this.measureChunk(font, t, style.letterSpacing);
14877
15400
  segments.push({
14878
15401
  t,
14879
15402
  m: block.runs[run].m,
15403
+ run,
14880
15404
  x,
14881
- width
15405
+ width,
15406
+ font,
15407
+ fontSize: entry.sizes[run]
14882
15408
  });
14883
15409
  x += width;
14884
15410
  i = j;
@@ -14910,12 +15436,18 @@ const LAYOUT_ATTRS = [
14910
15436
  * code written against `Konva.Text` keeps working.
14911
15437
  */
14912
15438
  var WeaveTextV2Shape = class extends Konva.Shape {
14913
- engine = new TextLayoutEngine();
14914
- naturalEngine = new TextLayoutEngine();
15439
+ engine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
15440
+ naturalEngine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
14915
15441
  layoutCache = null;
14916
15442
  blocksKey = {};
14917
15443
  blocksCache = [];
14918
15444
  viewport = null;
15445
+ indentStepValue;
15446
+ linkStyle = {
15447
+ color: "#1155ccff",
15448
+ hoverColor: "#3d7be0ff"
15449
+ };
15450
+ hoveredRun = null;
14919
15451
  constructor(config) {
14920
15452
  super(config);
14921
15453
  this.className = "WeaveTextV2Shape";
@@ -14932,6 +15464,55 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14932
15464
  this._setAttr("text", value == null ? "" : String(value));
14933
15465
  return this;
14934
15466
  }
15467
+ /**
15468
+ * List indent per depth, as a multiple of `fontSize`. Not a node attr: it
15469
+ * comes from the node config and must not be serialized into the document.
15470
+ */
15471
+ setIndentStep(step) {
15472
+ if (this.indentStepValue !== step) {
15473
+ this.indentStepValue = step;
15474
+ this.layoutCache = null;
15475
+ }
15476
+ return this;
15477
+ }
15478
+ /** Link run colours (from the node config; not serialized). */
15479
+ setLinkStyle(style) {
15480
+ this.linkStyle = style;
15481
+ return this;
15482
+ }
15483
+ /** Run under the pointer (`paragraph:run`), repainted with the hover colour. */
15484
+ setHoveredRun(key) {
15485
+ if (this.hoveredRun !== key) {
15486
+ this.hoveredRun = key;
15487
+ this.getLayer()?.batchDraw();
15488
+ }
15489
+ return this;
15490
+ }
15491
+ getHoveredRun() {
15492
+ return this.hoveredRun;
15493
+ }
15494
+ /**
15495
+ * Link run at a point in node-local coordinates (the legacy node-level
15496
+ * `link` wins: runs are then not links).
15497
+ */
15498
+ linkAtPoint(point) {
15499
+ if (this.attrs.link) return null;
15500
+ const layout = this.ensureLayout();
15501
+ const o = this.getContentOffset();
15502
+ const x = point.x - o.x;
15503
+ const y = point.y - o.y;
15504
+ for (const line of layout.lines) {
15505
+ if (y < line.y || y >= line.y + line.height) continue;
15506
+ for (const seg of line.segments) {
15507
+ const sx = line.offsetX + seg.x;
15508
+ if (x >= sx && x <= sx + seg.width && isSafeLink(seg.m?.link)) return {
15509
+ key: `${line.paragraph}:${seg.run}`,
15510
+ url: seg.m.link
15511
+ };
15512
+ }
15513
+ }
15514
+ return null;
15515
+ }
14935
15516
  /** Resolved blocks: stored or lazily synthesized (never written back). */
14936
15517
  getBlocks() {
14937
15518
  const k = this.blocksKey;
@@ -14952,6 +15533,10 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14952
15533
  }
14953
15534
  /** Replace content with blocks, keeping the `text` mirror in sync. */
14954
15535
  setBlocks(blocks) {
15536
+ if (!Array.isArray(blocks)) {
15537
+ this._setAttr("blocks", void 0);
15538
+ return this;
15539
+ }
14955
15540
  this._setAttr("blocks", blocks);
14956
15541
  this._setAttr("text", blocksToText(blocks));
14957
15542
  return this;
@@ -14989,6 +15574,16 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14989
15574
  textDecoration(v) {
14990
15575
  return this.accessor("textDecoration", "", v);
14991
15576
  }
15577
+ /**
15578
+ * Always stroke under the node transform. Weave turns `strokeScaleEnabled`
15579
+ * off while a node is transformed; Konva then resets the canvas transform
15580
+ * before stroking, which only works for path shapes. `strokeText` is not a
15581
+ * path, so the outline was painted at local coordinates on the canvas
15582
+ * origin at stage scale 1 (visible over other nodes while resizing).
15583
+ */
15584
+ getStrokeScaleEnabled() {
15585
+ return true;
15586
+ }
14992
15587
  accessor(name, fallback, value) {
14993
15588
  if (value === void 0) return this.attrs[name] ?? fallback;
14994
15589
  this.setAttr(name, value);
@@ -15023,7 +15618,8 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15023
15618
  lineHeight: this.lineHeight(),
15024
15619
  letterSpacing: this.letterSpacing(),
15025
15620
  wrap: this.wrap(),
15026
- align: this.align()
15621
+ align: this.align(),
15622
+ indentStep: this.indentStepValue
15027
15623
  };
15028
15624
  }
15029
15625
  /** Lazy, cached layout. Size accessors and painting force it. */
@@ -15053,14 +15649,13 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15053
15649
  };
15054
15650
  }
15055
15651
  /** Rendered width of a string with this shape's font and letter spacing. */
15056
- measureText(text) {
15057
- return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
15652
+ measureText(text, font) {
15653
+ return canvasMeasurer(font ?? fontString(this.style()), text) + text.length * this.letterSpacing();
15058
15654
  }
15059
15655
  /** Offset of the first line inside the box (padding + vertical align). */
15060
15656
  getContentOffset() {
15061
15657
  const padding = this.padding();
15062
- const layout = this.ensureLayout();
15063
- const used = layout.lines.length * layout.lineHeightPx + padding * 2;
15658
+ const used = this.ensureLayout().contentHeight + padding * 2;
15064
15659
  const va = this.verticalAlign();
15065
15660
  let alignY = 0;
15066
15661
  if (va === "middle") alignY = (this.getHeight() - used) / 2;
@@ -15098,8 +15693,9 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15098
15693
  const det = m.a * m.d - m.b * m.c;
15099
15694
  if (!det || !Number.isFinite(det)) return null;
15100
15695
  const canvas = ctx.getCanvas();
15101
- const w = canvas.getWidth();
15102
- const h = canvas.getHeight();
15696
+ const backing = canvas._canvas;
15697
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15698
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15103
15699
  if (!(w > 0) || !(h > 0)) return null;
15104
15700
  let y1 = Infinity;
15105
15701
  let y2 = -Infinity;
@@ -15123,30 +15719,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15123
15719
  const layout = self.ensureLayout();
15124
15720
  if (layout.lines.length === 0) return;
15125
15721
  const padding = self.padding();
15126
- const lineHeightPx = layout.lineHeightPx;
15127
15722
  let baseline = "middle";
15128
- let translateY = lineHeightPx / 2;
15129
- if (!Konva.legacyTextRendering) {
15130
- const m = self.measureMetrics();
15131
- baseline = "alphabetic";
15132
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15133
- }
15723
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15134
15724
  ctx.setAttr("font", fontString(self.style()));
15135
15725
  ctx.setAttr("textBaseline", baseline);
15136
15726
  ctx.setAttr("textAlign", "left");
15137
15727
  const alignY = self.verticalOffset(layout);
15138
15728
  const range = self.visibleRange(ctx);
15139
15729
  ctx.translate(padding, alignY + padding);
15140
- const top = alignY + padding;
15141
- for (let n = 0; n < layout.lines.length; n++) {
15142
- const lineTop = top + n * lineHeightPx;
15143
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15144
- self.paintLine(ctx, layout.lines[n], translateY + n * lineHeightPx);
15730
+ for (const line of layout.lines) {
15731
+ if (range && (alignY + padding + line.y + line.height < range.y1 - line.height || alignY + padding + line.y > range.y2 + line.height)) continue;
15732
+ self.paintLine(ctx, line, line.y + line.baseline);
15145
15733
  }
15146
15734
  };
15147
15735
  /** Vertical offset of the text block inside the box (verticalAlign). */
15148
15736
  verticalOffset(layout) {
15149
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15737
+ const used = layout.contentHeight + this.padding() * 2;
15150
15738
  const va = this.verticalAlign();
15151
15739
  if (va === "middle") return (this.getHeight() - used) / 2;
15152
15740
  if (va === "bottom") return this.getHeight() - used;
@@ -15157,7 +15745,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15157
15745
  const fontSize = this.fontSize();
15158
15746
  ctx.save();
15159
15747
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15160
- for (const seg of line.segments) this.paintSegment(ctx, seg.t, line.offsetX + seg.x, y);
15748
+ if (line.marker) {
15749
+ const mk = line.marker;
15750
+ ctx.setAttr("font", mk.font);
15751
+ this.fillText(ctx, mk.text, mk.x, y, mk.color);
15752
+ }
15753
+ for (const seg of line.segments) {
15754
+ ctx.setAttr("font", seg.font);
15755
+ const x = line.offsetX + seg.x;
15756
+ const isLink = !this.attrs.link && isSafeLink(seg.m?.link);
15757
+ let color = typeof seg.m?.color === "string" ? seg.m.color : void 0;
15758
+ if (isLink) color = this.hoveredRun === `${line.paragraph}:${seg.run}` ? this.linkStyle.hoverColor : this.linkStyle.color;
15759
+ this.paintSegment(ctx, seg, x, y, color);
15760
+ if (seg.m?.underline || isLink) this.strokeSegment(ctx, seg, x, y + Math.round(seg.fontSize / 4), color);
15761
+ if (seg.m?.strike) this.strokeSegment(ctx, seg, x, y - Math.round(seg.fontSize / 4), color);
15762
+ }
15161
15763
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15162
15764
  ctx.restore();
15163
15765
  }
@@ -15171,24 +15773,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15171
15773
  ctx.stroke();
15172
15774
  ctx.restore();
15173
15775
  }
15174
- paintSegment(ctx, text, x, y) {
15776
+ strokeSegment(ctx, seg, x, y, color) {
15777
+ ctx.save();
15778
+ ctx.beginPath();
15779
+ ctx.moveTo(x, y);
15780
+ ctx.lineTo(x + Math.round(seg.width), y);
15781
+ ctx.lineWidth = seg.fontSize / 15;
15782
+ ctx.strokeStyle = color ?? this.fill();
15783
+ ctx.stroke();
15784
+ ctx.restore();
15785
+ }
15786
+ paintSegment(ctx, seg, x, y, color) {
15175
15787
  const spacing = this.letterSpacing();
15176
15788
  if (spacing === 0) {
15177
- this.fillText(ctx, text, x, y);
15789
+ this.fillText(ctx, seg.t, x, y, color);
15178
15790
  return;
15179
15791
  }
15180
15792
  let cx = x;
15181
- for (const ch of Array.from(text)) {
15182
- this.fillText(ctx, ch, cx, y);
15183
- cx += this.measureSize(ch).width + spacing;
15793
+ for (const ch of Array.from(seg.t)) {
15794
+ this.fillText(ctx, ch, cx, y, color);
15795
+ cx += canvasMeasurer(seg.font, ch) + spacing;
15184
15796
  }
15185
15797
  }
15186
- fillText(ctx, text, x, y) {
15798
+ fillText(ctx, text, x, y, color) {
15187
15799
  const self = this;
15188
15800
  self._partialText = text;
15189
15801
  self._partialTextX = x;
15190
15802
  self._partialTextY = y;
15191
- ctx.fillStrokeShape(this);
15803
+ if (color === void 0) {
15804
+ ctx.fillStrokeShape(this);
15805
+ return;
15806
+ }
15807
+ const prev = this.attrs.fill;
15808
+ this.attrs.fill = color;
15809
+ try {
15810
+ ctx.fillStrokeShape(this);
15811
+ } finally {
15812
+ this.attrs.fill = prev;
15813
+ }
15192
15814
  }
15193
15815
  _hitFunc = (ctx, shape) => {
15194
15816
  ctx.beginPath();
@@ -15304,9 +15926,27 @@ function lineIndexAt(layout, offset) {
15304
15926
  }
15305
15927
  return fallback;
15306
15928
  }
15929
+ /** Width of the first `k` UTF-16 units of a line, run by run. */
15930
+ function prefixWidth(line, k, measure) {
15931
+ if (k <= 0) return 0;
15932
+ if (line.segments.length === 0) return measure(line.text.slice(0, k));
15933
+ let width = 0;
15934
+ let pos = 0;
15935
+ for (const seg of line.segments) {
15936
+ const end = pos + seg.t.length;
15937
+ if (k >= end) width += seg.width;
15938
+ else {
15939
+ width += measure(seg.t.slice(0, k - pos), seg.font);
15940
+ break;
15941
+ }
15942
+ pos = end;
15943
+ if (k === end) break;
15944
+ }
15945
+ return width;
15946
+ }
15307
15947
  function lineX(line, offset, measure) {
15308
15948
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15309
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15949
+ return line.offsetX + prefixWidth(line, k, measure);
15310
15950
  }
15311
15951
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15312
15952
  function caretRect(layout, offset, measure) {
@@ -15320,16 +15960,22 @@ function caretRect(layout, offset, measure) {
15320
15960
  };
15321
15961
  return {
15322
15962
  x: lineX(line, offset, measure),
15323
- y: i * layout.lineHeightPx,
15324
- height: layout.lineHeightPx,
15963
+ y: line.y,
15964
+ height: line.height,
15325
15965
  line: i
15326
15966
  };
15327
15967
  }
15968
+ /** Index of the line containing a local y (clamped). */
15969
+ function lineIndexAtY(layout, y) {
15970
+ const lines = layout.lines;
15971
+ for (let i = 0; i < lines.length; i++) if (y < lines[i].y + lines[i].height) return i;
15972
+ return Math.max(0, lines.length - 1);
15973
+ }
15328
15974
  /** Offset closest to a point in text-local coordinates. */
15329
15975
  function offsetFromPoint(layout, point, measure) {
15330
15976
  const lines = layout.lines;
15331
15977
  if (lines.length === 0) return 0;
15332
- return offsetInLine(lines[Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / layout.lineHeightPx)))], point.x, measure);
15978
+ return offsetInLine(lines[lineIndexAtY(layout, point.y)], point.x, measure);
15333
15979
  }
15334
15980
  function offsetInLine(line, x, measure) {
15335
15981
  const rel = x - line.offsetX;
@@ -15337,7 +15983,7 @@ function offsetInLine(line, x, measure) {
15337
15983
  const b = boundaries(line.text, graphemeSeg);
15338
15984
  let prevW = 0;
15339
15985
  for (let k = 1; k < b.length; k++) {
15340
- const w = measure(line.text.slice(0, b[k]));
15986
+ const w = prefixWidth(line, b[k], measure);
15341
15987
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15342
15988
  prevW = w;
15343
15989
  }
@@ -15361,7 +16007,7 @@ function selectionRects(layout, from, to, measure) {
15361
16007
  if (from > to) [from, to] = [to, from];
15362
16008
  if (from === to) return [];
15363
16009
  const out = [];
15364
- layout.lines.forEach((line, i) => {
16010
+ layout.lines.forEach((line) => {
15365
16011
  const hasNewline = line.lastInParagraph;
15366
16012
  const lineFrom = Math.max(from, line.start);
15367
16013
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15372,9 +16018,9 @@ function selectionRects(layout, from, to, measure) {
15372
16018
  if (x2 <= x1) return;
15373
16019
  out.push({
15374
16020
  x: x1,
15375
- y: i * layout.lineHeightPx,
16021
+ y: line.y,
15376
16022
  width: x2 - x1,
15377
- height: layout.lineHeightPx
16023
+ height: line.height
15378
16024
  });
15379
16025
  });
15380
16026
  return out;
@@ -15393,6 +16039,11 @@ var TextEditModel = class {
15393
16039
  focus = 0;
15394
16040
  /** x kept while moving vertically; reset by any other move. */
15395
16041
  desiredX = null;
16042
+ /**
16043
+ * Marks the next typed text takes at a collapsed caret (e.g. bold toggled
16044
+ * with nothing selected). Cleared when the selection moves.
16045
+ */
16046
+ storedMarks = null;
15396
16047
  undoStack = [];
15397
16048
  redoStack = [];
15398
16049
  lastKind = null;
@@ -15429,12 +16080,14 @@ var TextEditModel = class {
15429
16080
  const len = this.getText().length;
15430
16081
  this.anchor = Math.max(0, Math.min(anchor, len));
15431
16082
  this.focus = Math.max(0, Math.min(focus, len));
16083
+ this.storedMarks = null;
15432
16084
  this.breakCoalescing();
15433
16085
  }
15434
16086
  moveTo(offset, extend = false, keepDesiredX = false) {
15435
16087
  const len = this.getText().length;
15436
16088
  const o = Math.max(0, Math.min(offset, len));
15437
16089
  this.focus = o;
16090
+ this.storedMarks = null;
15438
16091
  if (!extend) this.anchor = o;
15439
16092
  if (!keepDesiredX) this.desiredX = null;
15440
16093
  this.breakCoalescing();
@@ -15442,6 +16095,7 @@ var TextEditModel = class {
15442
16095
  selectAll() {
15443
16096
  this.anchor = 0;
15444
16097
  this.focus = this.getText().length;
16098
+ this.storedMarks = null;
15445
16099
  this.breakCoalescing();
15446
16100
  }
15447
16101
  breakCoalescing() {
@@ -15477,7 +16131,14 @@ var TextEditModel = class {
15477
16131
  end = Math.max(start, Math.min(end, len));
15478
16132
  if (start === end && text === "") return false;
15479
16133
  this.record(kind, start);
15480
- this.blocks = replaceRange(this.blocks, start, end, text);
16134
+ const carried = this.storedMarks ?? marksAtOffset(this.blocks, start);
16135
+ this.blocks = replaceRange(this.blocks, start, end, text, this.storedMarks ?? void 0);
16136
+ this.storedMarks = null;
16137
+ if (text.endsWith("\n") && carried) {
16138
+ const { link: _link, ...rest } = carried;
16139
+ const next = cleanMarks(rest);
16140
+ if (next && !marksAtOffset(this.blocks, start + text.length)) this.storedMarks = next;
16141
+ }
15481
16142
  this.anchor = this.focus = start + text.length;
15482
16143
  this.lastEditEnd = this.focus;
15483
16144
  this.desiredX = null;
@@ -15517,14 +16178,102 @@ var TextEditModel = class {
15517
16178
  this.restore(next);
15518
16179
  return true;
15519
16180
  }
16181
+ /** Current marks at the caret / selection start (stored marks win). */
16182
+ caretMarks() {
16183
+ return this.storedMarks ?? marksAtOffset(this.blocks, this.getSelection().start);
16184
+ }
16185
+ /** Always one new undo entry, never coalesced. */
16186
+ commitFormat(next) {
16187
+ if (JSON.stringify(next) === JSON.stringify(this.blocks)) return false;
16188
+ this.lastKind = null;
16189
+ this.record("format", -1);
16190
+ this.lastKind = null;
16191
+ this.blocks = next;
16192
+ return true;
16193
+ }
16194
+ /** Set (`undefined` clears) one mark on the selection or the stored marks. */
16195
+ setMark(key, value) {
16196
+ const { start, end } = this.getSelection();
16197
+ if (start === end) {
16198
+ const base = { ...this.caretMarks() ?? {} };
16199
+ if (value === void 0 || value === false) delete base[key];
16200
+ else base[key] = value;
16201
+ this.storedMarks = cleanMarks(base) ?? {};
16202
+ return true;
16203
+ }
16204
+ return this.commitFormat(applyMark(this.blocks, start, end, key, value));
16205
+ }
16206
+ /** Toggle a boolean mark: on unless the whole selection already has it. */
16207
+ toggleMark(key) {
16208
+ const { start, end } = this.getSelection();
16209
+ const q = queryMark(this.blocks, start, end, key, this.storedMarks);
16210
+ return this.setMark(key, q.state === "all" ? void 0 : true);
16211
+ }
16212
+ toggleList(kind) {
16213
+ const { start, end } = this.getSelection();
16214
+ return this.commitFormat(toggleList(this.blocks, start, end, kind));
16215
+ }
16216
+ setAlign(align) {
16217
+ const { start, end } = this.getSelection();
16218
+ return this.commitFormat(setBlockAlign(this.blocks, start, end, align));
16219
+ }
16220
+ indent() {
16221
+ const { start, end } = this.getSelection();
16222
+ return this.commitFormat(indentBlocks(this.blocks, start, end));
16223
+ }
16224
+ outdent() {
16225
+ const { start, end } = this.getSelection();
16226
+ return this.commitFormat(outdentBlocks(this.blocks, start, end));
16227
+ }
16228
+ blockAt(offset) {
16229
+ let pos = 0;
16230
+ for (let i = 0; i < this.blocks.length; i++) {
16231
+ const len = blockText(this.blocks[i]).length;
16232
+ if (offset <= pos + len) return {
16233
+ index: i,
16234
+ start: pos,
16235
+ length: len
16236
+ };
16237
+ pos += len + 1;
16238
+ }
16239
+ }
16240
+ /**
16241
+ * Enter in a list: an empty item exits the list; otherwise the item
16242
+ * continues (the block split carries `kind` and `depth`).
16243
+ */
16244
+ enter() {
16245
+ const { start, end } = this.getSelection();
16246
+ const b = start === end ? this.blockAt(start) : void 0;
16247
+ if (b && this.blocks[b.index].kind && b.length === 0) return this.commitFormat(outdentToPlain(this.blocks, b.index));
16248
+ return this.replaceRange(start, end, "\n", "other");
16249
+ }
16250
+ /**
16251
+ * Backspace at the start of a list item outdents it, or removes the
16252
+ * marker at depth 0. Returns false when the key is not handled here.
16253
+ */
16254
+ backspaceInList() {
16255
+ const { start, end } = this.getSelection();
16256
+ if (start !== end) return false;
16257
+ const b = this.blockAt(start);
16258
+ if (!b || b.start !== start || !this.blocks[b.index].kind) return false;
16259
+ return this.commitFormat(outdentBlocks(this.blocks, start, start));
16260
+ }
15520
16261
  restore(s) {
15521
16262
  this.blocks = s.blocks;
16263
+ this.storedMarks = null;
15522
16264
  this.anchor = s.anchor;
15523
16265
  this.focus = s.focus;
15524
16266
  this.lastKind = null;
15525
16267
  this.desiredX = null;
15526
16268
  }
15527
16269
  };
16270
+ function outdentToPlain(blocks, index) {
16271
+ return blocks.map((b, i) => {
16272
+ if (i !== index) return b;
16273
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
16274
+ return rest;
16275
+ });
16276
+ }
15528
16277
  const SLICE_BUDGET_MS = 8;
15529
16278
  const BLINK_MS = 530;
15530
16279
  var TextEditSession = class {
@@ -15582,7 +16331,7 @@ var TextEditSession = class {
15582
16331
  };
15583
16332
  if (this.host.selectAllOnStart) this.model.selectAll();
15584
16333
  this.syncTextarea();
15585
- ta.focus();
16334
+ ta.focus({ preventScroll: true });
15586
16335
  this.positionTextarea();
15587
16336
  this.startBlink();
15588
16337
  this.repaint();
@@ -15611,7 +16360,7 @@ var TextEditSession = class {
15611
16360
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15612
16361
  }
15613
16362
  focus() {
15614
- this.textarea?.focus();
16363
+ this.textarea?.focus({ preventScroll: true });
15615
16364
  }
15616
16365
  createTextarea() {
15617
16366
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15660,8 +16409,12 @@ var TextEditSession = class {
15660
16409
  if (!ta) return;
15661
16410
  const c = this.caretStagePoint();
15662
16411
  const p = this.host.stageToContainer(c);
15663
- ta.style.left = `${p.x}px`;
15664
- ta.style.top = `${p.y}px`;
16412
+ this.host.revealCaret?.(p);
16413
+ const box = this.host.container;
16414
+ const maxX = Math.max(0, (box.clientWidth || Infinity) - 2);
16415
+ const maxY = Math.max(0, (box.clientHeight || Infinity) - 20);
16416
+ ta.style.left = `${Math.min(Math.max(p.x, 0), maxX)}px`;
16417
+ ta.style.top = `${Math.min(Math.max(p.y, 0), maxY)}px`;
15665
16418
  }
15666
16419
  caretStagePoint() {
15667
16420
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15693,10 +16446,41 @@ var TextEditSession = class {
15693
16446
  if (e.key === "Escape") this.host.onRequestExit("escape");
15694
16447
  else if (isUndo) this.undo();
15695
16448
  else if (isRedo) this.redo();
16449
+ else if (this.handleRichKey(e, mod, key)) return true;
15696
16450
  else if (e.key === "Tab") return true;
15697
16451
  else return false;
15698
16452
  return true;
15699
16453
  }
16454
+ /** Format shortcuts and list key rules; only when rich text is enabled. */
16455
+ handleRichKey(e, mod, key) {
16456
+ if (!this.host.richText) return false;
16457
+ if (mod && !e.shiftKey && !e.altKey) {
16458
+ const marks = {
16459
+ b: "bold",
16460
+ i: "italic",
16461
+ u: "underline"
16462
+ };
16463
+ if (marks[key]) {
16464
+ this.toggleMark(marks[key]);
16465
+ return true;
16466
+ }
16467
+ }
16468
+ if (mod || this.composing) return false;
16469
+ if (e.key === "Tab") {
16470
+ if (e.shiftKey) this.outdent();
16471
+ else this.indent();
16472
+ return true;
16473
+ }
16474
+ if (e.key === "Enter") {
16475
+ if (this.model.enter()) this.afterFormat("command", true);
16476
+ return true;
16477
+ }
16478
+ if (e.key === "Backspace" && this.model.backspaceInList()) {
16479
+ this.afterFormat("command", true);
16480
+ return true;
16481
+ }
16482
+ return false;
16483
+ }
15700
16484
  handleNavigationKey(e) {
15701
16485
  if (e.ctrlKey || e.metaKey) return false;
15702
16486
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15750,7 +16534,15 @@ var TextEditSession = class {
15750
16534
  while (p < max && oldText[p] === newText[p]) p++;
15751
16535
  let s = 0;
15752
16536
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15753
- const inserted = newText.slice(p, newText.length - s);
16537
+ let inserted = newText.slice(p, newText.length - s);
16538
+ if (s + p === oldText.length && inserted !== "") {
16539
+ const q = ta.selectionEnd - inserted.length;
16540
+ if (q >= 0 && q < p && newText.slice(q, q + inserted.length) === inserted && oldText.slice(0, q) + inserted + oldText.slice(q) === newText) {
16541
+ s = oldText.length - q;
16542
+ p = q;
16543
+ inserted = newText.slice(p, newText.length - s);
16544
+ }
16545
+ }
15754
16546
  const inputType = e.inputType ?? "";
15755
16547
  const kind = this.editKindFor(inputType);
15756
16548
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15791,6 +16583,7 @@ var TextEditSession = class {
15791
16583
  if (this.model.undo()) {
15792
16584
  this.syncTextarea();
15793
16585
  this.afterContentChange(0, true);
16586
+ this.host.onFormatChange?.("undo");
15794
16587
  }
15795
16588
  }
15796
16589
  redo() {
@@ -15798,8 +16591,42 @@ var TextEditSession = class {
15798
16591
  if (this.model.redo()) {
15799
16592
  this.syncTextarea();
15800
16593
  this.afterContentChange(0, true);
16594
+ this.host.onFormatChange?.("redo");
15801
16595
  }
15802
16596
  }
16597
+ afterFormat(reason, content = false) {
16598
+ this.flushPending();
16599
+ this.syncTextarea();
16600
+ if (content) this.afterContentChange(0, true);
16601
+ else this.applyContent();
16602
+ this.afterSelectionChange();
16603
+ this.host.onFormatChange?.(reason);
16604
+ }
16605
+ toggleMark(key) {
16606
+ if (this.model.toggleMark(key)) this.afterFormat("command");
16607
+ }
16608
+ setMark(key, value) {
16609
+ if (this.model.setMark(key, value)) this.afterFormat("command");
16610
+ }
16611
+ toggleList(kind) {
16612
+ if (this.model.toggleList(kind)) this.afterFormat("command");
16613
+ }
16614
+ setAlign(align) {
16615
+ if (this.model.setAlign(align)) this.afterFormat("command");
16616
+ }
16617
+ indent() {
16618
+ if (this.model.indent()) this.afterFormat("command");
16619
+ }
16620
+ outdent() {
16621
+ if (this.model.outdent()) this.afterFormat("command");
16622
+ }
16623
+ getSelection() {
16624
+ return this.model.getSelection();
16625
+ }
16626
+ getFormat() {
16627
+ const { start, end } = this.model.getSelection();
16628
+ return getSelectionFormat(this.model.getBlocks(), start, end, this.target.getFontSize(), this.model.storedMarks);
16629
+ }
15803
16630
  toLocal(e) {
15804
16631
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15805
16632
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15814,12 +16641,26 @@ var TextEditSession = class {
15814
16641
  const { width, height } = this.target.getSize();
15815
16642
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15816
16643
  }
16644
+ /**
16645
+ * True for pointer targets inside an element the app marked with
16646
+ * `data-weave-text-edit-keep` (e.g. a formatting toolbar): they neither
16647
+ * end the session nor, unless they are form fields, steal focus.
16648
+ */
16649
+ isKeepTarget(e) {
16650
+ const t = e.target;
16651
+ return typeof Element !== "undefined" && t instanceof Element && !!t.closest("[data-weave-text-edit-keep]");
16652
+ }
15817
16653
  onMouseDown = (e) => {
16654
+ if (this.isKeepTarget(e)) {
16655
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16656
+ return;
16657
+ }
15818
16658
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15819
16659
  };
15820
16660
  onPointerDown = (e) => {
15821
16661
  if (e.button !== 0 && e.pointerType === "mouse") return;
15822
16662
  if (this.textarea && e.target === this.textarea) return;
16663
+ if (this.isKeepTarget(e)) return;
15823
16664
  const local = this.toLocal(e);
15824
16665
  if (!this.isInside(local)) {
15825
16666
  e.stopPropagation();
@@ -15830,10 +16671,11 @@ var TextEditSession = class {
15830
16671
  e.preventDefault();
15831
16672
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15832
16673
  const text = this.model.getText();
15833
- if (e.detail >= 3) {
16674
+ const clicks = this.countClicks(e);
16675
+ if (clicks >= 3) {
15834
16676
  const [s, en] = paragraphRangeAt(text, offset);
15835
16677
  this.model.setSelection(s, en);
15836
- } else if (e.detail === 2) {
16678
+ } else if (clicks === 2) {
15837
16679
  const [s, en] = wordRangeAt(text, offset);
15838
16680
  this.model.setSelection(s, en);
15839
16681
  } else {
@@ -15842,8 +16684,31 @@ var TextEditSession = class {
15842
16684
  }
15843
16685
  this.syncTextarea();
15844
16686
  this.afterSelectionChange();
15845
- this.textarea?.focus();
16687
+ this.textarea?.focus({ preventScroll: true });
15846
16688
  };
16689
+ lastClick = {
16690
+ t: 0,
16691
+ x: 0,
16692
+ y: 0,
16693
+ n: 0
16694
+ };
16695
+ /**
16696
+ * Browsers report `detail` as 0 on pointerdown, so multi-click is counted
16697
+ * here (same spot, within 500 ms). A real `detail` still wins.
16698
+ */
16699
+ countClicks(e) {
16700
+ const now = e.timeStamp || Date.now();
16701
+ const l = this.lastClick;
16702
+ const near = now - l.t < 500 && Math.hypot(e.clientX - l.x, e.clientY - l.y) < 5 && l.n > 0;
16703
+ const n = Math.max(e.detail, near ? l.n + 1 : 1);
16704
+ this.lastClick = {
16705
+ t: now,
16706
+ x: e.clientX,
16707
+ y: e.clientY,
16708
+ n: n >= 3 ? 0 : n
16709
+ };
16710
+ return n;
16711
+ }
15847
16712
  onPointerMove = (e) => {
15848
16713
  if (!this.dragging) return;
15849
16714
  const local = this.toLocal(e);
@@ -15855,13 +16720,14 @@ var TextEditSession = class {
15855
16720
  onPointerUp = () => {
15856
16721
  if (!this.dragging) return;
15857
16722
  this.dragging = false;
15858
- this.textarea?.focus();
16723
+ this.textarea?.focus({ preventScroll: true });
15859
16724
  };
15860
16725
  afterSelectionChange() {
15861
16726
  this.syncTextarea();
15862
16727
  this.positionTextarea();
15863
16728
  this.resetBlink();
15864
16729
  this.repaint();
16730
+ this.host.onSelectionChange?.();
15865
16731
  }
15866
16732
  afterContentChange(insertedLength, immediate = false) {
15867
16733
  this.flushPending();
@@ -15885,6 +16751,7 @@ var TextEditSession = class {
15885
16751
  applyContent() {
15886
16752
  this.target.setBlocks(this.model.getBlocks());
15887
16753
  this.host.onChange?.(this.model.getBlocks());
16754
+ this.host.onSelectionChange?.();
15888
16755
  this.positionTextarea();
15889
16756
  this.target.requestRedraw();
15890
16757
  this.repaint();
@@ -15960,15 +16827,29 @@ function host_signal(signal, abort) {
15960
16827
  //#region src/nodes/text-v2/constants.ts
15961
16828
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15962
16829
  const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
16830
+ /** Default list indent per depth, as a multiple of the node `fontSize`. */
16831
+ const WEAVE_TEXT_V2_DEFAULT_INDENT_STEP = 1.5;
15963
16832
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15964
16833
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15965
16834
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15966
- edition: { outline: {
15967
- enabled: true,
15968
- color: "#0074ff",
15969
- width: 1,
15970
- padding: 0
15971
- } },
16835
+ edition: {
16836
+ outline: {
16837
+ enabled: true,
16838
+ color: "#0074ff",
16839
+ width: 1,
16840
+ padding: 0
16841
+ },
16842
+ followPadding: {
16843
+ top: 40,
16844
+ right: 40,
16845
+ bottom: 40,
16846
+ left: 40
16847
+ }
16848
+ },
16849
+ richText: {
16850
+ enabled: false,
16851
+ indentStep: WEAVE_TEXT_V2_DEFAULT_INDENT_STEP
16852
+ },
15972
16853
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15973
16854
  };
15974
16855
  //#endregion
@@ -15996,6 +16877,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15996
16877
  editingNodeId = null;
15997
16878
  session = null;
15998
16879
  sizeWarned = false;
16880
+ selectionTimer;
15999
16881
  nodeRenderedAddedRegistered = false;
16000
16882
  keyPressHandler;
16001
16883
  constructor(params) {
@@ -16069,6 +16951,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16069
16951
  linkPreviousTextDecoration
16070
16952
  }
16071
16953
  });
16954
+ text.setIndentStep(this.indentStep());
16955
+ text.setLinkStyle({
16956
+ color: this.config.link.defaultColor,
16957
+ hoverColor: this.config.link.hoverColor
16958
+ });
16072
16959
  this.setupDefaultNodeAugmentation(text);
16073
16960
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16074
16961
  text.getTransformerProperties = function() {
@@ -16164,7 +17051,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16164
17051
  this.applySize(shape);
16165
17052
  },
16166
17053
  getLayout: () => shape.ensureLayout(),
16167
- measure: (t) => shape.measureText(t),
17054
+ measure: (t, f) => shape.measureText(t, f),
16168
17055
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16169
17056
  getContentOffset: () => shape.getContentOffset(),
16170
17057
  getSize: () => ({
@@ -16215,7 +17102,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16215
17102
  y: p.y * sy
16216
17103
  };
16217
17104
  },
17105
+ revealCaret: (p) => {
17106
+ const box = container;
17107
+ const w = box.clientWidth;
17108
+ const h = box.clientHeight;
17109
+ if (!w || !h) return;
17110
+ const f = this.config.edition.followPadding;
17111
+ const dx = p.x < f.left ? f.left - p.x : p.x > w - f.right ? w - f.right - p.x : 0;
17112
+ const dy = p.y < f.top ? f.top - p.y : p.y > h - f.bottom ? h - f.bottom - p.y : 0;
17113
+ if (!dx && !dy) return;
17114
+ stage.x(stage.x() + dx);
17115
+ stage.y(stage.y() + dy);
17116
+ this.instance.emitEvent("onStageMove");
17117
+ },
16218
17118
  selectAllOnStart: !create,
17119
+ richText: this.isRich(shape),
17120
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17121
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16219
17122
  caretColor: this.config.cursor.color,
16220
17123
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16221
17124
  ...this.config.edition.outline.enabled && {
@@ -16251,6 +17154,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16251
17154
  exitEditMode(shape) {
16252
17155
  const session = this.session;
16253
17156
  if (!session || !this.editing) return;
17157
+ clearTimeout(this.selectionTimer);
16254
17158
  const { changed } = session.getResult();
16255
17159
  session.destroy();
16256
17160
  this.session = null;
@@ -16313,11 +17217,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16313
17217
  text.getLayer()?.batchDraw();
16314
17218
  }
16315
17219
  const layout = text.getAttrs().layout;
16316
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16317
- text.width(text.width() * text.scaleX());
16318
- resetScale(text);
16319
- text.fontSize(text.fontSize() * text.scaleY());
16320
- }
17220
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16321
17221
  text.setAttr("shouldUpdateOnTransform", false);
16322
17222
  text.getLayer()?.batchDraw();
16323
17223
  });
@@ -16361,6 +17261,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16361
17261
  };
16362
17262
  const isLinked = Boolean(nextProps.link);
16363
17263
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17264
+ shape.setIndentStep(this.indentStep());
16364
17265
  shape.setAttrs({
16365
17266
  ...nextProps,
16366
17267
  ...this.outlineProps(),
@@ -16398,7 +17299,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16398
17299
  delete cleanedAttrs.dragBoundFunc;
16399
17300
  if (cleanedAttrs.link) {
16400
17301
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16401
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17302
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16402
17303
  }
16403
17304
  return {
16404
17305
  key: attrs.id ?? "",
@@ -16413,18 +17314,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16413
17314
  }
16414
17315
  };
16415
17316
  }
17317
+ linkCtxMenuGuardRegistered = false;
17318
+ linkHoverText = null;
17319
+ linkModifier = false;
17320
+ linkHoverRefresh = null;
17321
+ linkHitAt(text) {
17322
+ const link = text.getAttrs().link;
17323
+ if (link) {
17324
+ const p = this.localPointer(text);
17325
+ return p.x >= 0 && p.y >= 0 && p.x <= text.width() && p.y <= text.height() ? { url: link } : null;
17326
+ }
17327
+ return text.linkAtPoint(this.localPointer(text));
17328
+ }
17329
+ registerLinkContextMenuGuard() {
17330
+ if (this.linkCtxMenuGuardRegistered || this.instance.isServerSide()) return;
17331
+ this.linkCtxMenuGuardRegistered = true;
17332
+ const stage = this.instance.getStage();
17333
+ const signal = this.instance.getEventsController().signal;
17334
+ const syncModifier = (e) => {
17335
+ const next = e.ctrlKey || e.metaKey;
17336
+ if (next === this.linkModifier) return;
17337
+ this.linkModifier = next;
17338
+ this.linkHoverRefresh?.();
17339
+ };
17340
+ window.addEventListener("keydown", syncModifier, { signal });
17341
+ window.addEventListener("keyup", syncModifier, { signal });
17342
+ window.addEventListener("blur", () => {
17343
+ this.linkModifier = false;
17344
+ this.linkHoverRefresh?.();
17345
+ }, { signal });
17346
+ stage.container().addEventListener("contextmenu", (e) => {
17347
+ if (this.editing || !e.ctrlKey) return;
17348
+ const text = this.linkHoverText;
17349
+ if (!text) return;
17350
+ stage.setPointersPositions(e);
17351
+ const hit = this.linkHitAt(text);
17352
+ if (!hit) return;
17353
+ e.preventDefault();
17354
+ e.stopPropagation();
17355
+ this.openUrl(hit.url);
17356
+ }, {
17357
+ capture: true,
17358
+ signal: this.instance.getEventsController().signal
17359
+ });
17360
+ }
16416
17361
  setupLinkBehavior(text) {
16417
- text.handleMouseover = () => {
16418
- if (!text.getAttrs().link) return;
16419
- text.fill(this.config.link.hoverColor);
16420
- text.getLayer()?.batchDraw();
17362
+ this.registerLinkContextMenuGuard();
17363
+ const container = () => this.instance.getStage().container();
17364
+ const idleCursor = () => this.isNodeSelected(text) ? "grab" : "pointer";
17365
+ const applyHover = () => {
17366
+ if (this.editing) return;
17367
+ this.linkHoverText = text;
17368
+ const hit = this.linkModifier ? this.linkHitAt(text) : null;
17369
+ if (!text.getAttrs().link) text.setHoveredRun(hit?.key ?? null);
17370
+ else {
17371
+ text.fill(hit ? this.config.link.hoverColor : this.config.link.defaultColor);
17372
+ text.getLayer()?.batchDraw();
17373
+ }
17374
+ container().style.cursor = hit ? "pointer" : idleCursor();
17375
+ };
17376
+ const updateHover = (e) => {
17377
+ if (e?.evt) this.linkModifier = e.evt.ctrlKey || e.evt.metaKey;
17378
+ this.linkHoverRefresh = applyHover;
17379
+ applyHover();
17380
+ };
17381
+ text.handleMouseover = (e) => {
17382
+ updateHover(e);
16421
17383
  };
16422
17384
  text.handleMouseout = () => {
17385
+ if (this.linkHoverText === text) {
17386
+ this.linkHoverText = null;
17387
+ this.linkHoverRefresh = null;
17388
+ }
17389
+ text.setHoveredRun(null);
16423
17390
  if (!text.getAttrs().link) return;
16424
17391
  text.fill(this.config.link.defaultColor);
16425
17392
  text.getLayer()?.batchDraw();
16426
17393
  };
16427
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17394
+ text.on("pointermove", updateHover);
17395
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16428
17396
  let linkClickTimeout;
16429
17397
  const clearLinkClickTimeout = () => {
16430
17398
  if (linkClickTimeout) {
@@ -16438,27 +17406,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16438
17406
  previousDblClick();
16439
17407
  };
16440
17408
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16441
- if (!text.getAttrs().link) return;
17409
+ const runLink = text.getAttrs().link ? null : text.linkAtPoint(this.localPointer(text));
17410
+ if (!text.getAttrs().link && !runLink) return;
17411
+ const open = () => runLink ? this.openUrl(runLink.url) : this.openTextLink(text);
16442
17412
  if (ctrlOrMetaPressed) {
16443
17413
  clearLinkClickTimeout();
16444
- this.openTextLink(text);
17414
+ open();
16445
17415
  return;
16446
17416
  }
16447
17417
  if (wasSelected) {
16448
17418
  clearLinkClickTimeout();
16449
17419
  linkClickTimeout = setTimeout(() => {
16450
17420
  linkClickTimeout = void 0;
16451
- this.openTextLink(text);
17421
+ open();
16452
17422
  }, Konva.dblClickWindow);
16453
17423
  }
16454
17424
  };
16455
17425
  }
16456
- openTextLink(textNode) {
17426
+ localPointer(text) {
17427
+ return text.getRelativePointerPosition() ?? {
17428
+ x: -1,
17429
+ y: -1
17430
+ };
17431
+ }
17432
+ lastLinkOpen = 0;
17433
+ openUrl(link) {
16457
17434
  if (this.instance.isServerSide()) return;
16458
- const link = textNode.getAttrs().link;
16459
17435
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17436
+ const now = Date.now();
17437
+ if (now - this.lastLinkOpen < 500) return;
17438
+ this.lastLinkOpen = now;
16460
17439
  window.open(link, "_blank", "noopener,noreferrer");
16461
17440
  }
17441
+ openTextLink(textNode) {
17442
+ this.openUrl(textNode.getAttrs().link);
17443
+ }
16462
17444
  getLink(nodeInstance) {
16463
17445
  const link = nodeInstance.getAttrs().link;
16464
17446
  return typeof link === "string" ? link : void 0;
@@ -16478,6 +17460,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16478
17460
  delete serialized.props.linkPreviousTextDecoration;
16479
17461
  this.instance.updateNode(serialized);
16480
17462
  }
17463
+ indentStep() {
17464
+ const step = this.config.richText.indentStep;
17465
+ return Number.isFinite(step) && step > 0 ? step : 1.5;
17466
+ }
17467
+ /**
17468
+ * Rich text is on for a node when its `richText` attribute says so;
17469
+ * nodes without the attribute follow `config.richText.enabled`.
17470
+ */
17471
+ isRich(nodeInstance) {
17472
+ const own = nodeInstance.getAttrs().richText;
17473
+ return typeof own === "boolean" ? own : this.config.richText.enabled;
17474
+ }
17475
+ isRichTextEnabled(nodeInstance) {
17476
+ return nodeInstance ? this.isRich(nodeInstance) : this.config.richText.enabled;
17477
+ }
17478
+ activeSession(shape) {
17479
+ return this.editing && this.editingNodeId === shape.id() ? this.session : null;
17480
+ }
17481
+ emitFormatChange(shape, reason) {
17482
+ this.instance.emitEvent("onTextFormatChange", {
17483
+ node: shape,
17484
+ format: this.getSelectionFormat(shape),
17485
+ reason
17486
+ });
17487
+ }
17488
+ /** Selection events fire often while dragging: coalesce them. */
17489
+ scheduleSelectionEvent(shape) {
17490
+ if (this.selectionTimer) return;
17491
+ this.selectionTimer = setTimeout(() => {
17492
+ this.selectionTimer = void 0;
17493
+ const session = this.activeSession(shape);
17494
+ if (!session) return;
17495
+ const { start, end } = session.getSelection();
17496
+ this.instance.emitEvent("onTextSelectionChange", {
17497
+ node: shape,
17498
+ start,
17499
+ end,
17500
+ format: session.getFormat()
17501
+ });
17502
+ }, 50);
17503
+ }
17504
+ /**
17505
+ * Format of the active selection while editing, otherwise of `range`
17506
+ * (default: the whole text). Sizes are px.
17507
+ */
17508
+ getSelectionFormat(nodeInstance, range) {
17509
+ const shape = nodeInstance;
17510
+ const session = this.activeSession(shape);
17511
+ if (session) return session.getFormat();
17512
+ const blocks = shape.getBlocks();
17513
+ const r = range ?? {
17514
+ start: 0,
17515
+ end: blocksToText(blocks).length
17516
+ };
17517
+ return getSelectionFormat(blocks, r.start, r.end, shape.fontSize());
17518
+ }
17519
+ /**
17520
+ * Run a block transform on a non-editing node: one `updateNode`, so one
17521
+ * Weave undo step. Returns false when rich text is disabled.
17522
+ */
17523
+ commandOnNode(shape, range, fn) {
17524
+ const blocks = shape.getBlocks();
17525
+ const next = fn(blocks, range?.start ?? 0, range?.end ?? blocksToText(blocks).length);
17526
+ if (JSON.stringify(next) === JSON.stringify(blocks)) return false;
17527
+ shape.setBlocks(next);
17528
+ this.applySize(shape);
17529
+ this.instance.updateNode(this.serialize(shape));
17530
+ this.emitFormatChange(shape, "command");
17531
+ return true;
17532
+ }
17533
+ /**
17534
+ * Set a mark (`undefined` clears it). `size` is in px. While the node is
17535
+ * being edited it applies to the editor selection, otherwise to `range`
17536
+ * (default: the whole text).
17537
+ */
17538
+ applyMark(nodeInstance, key, value, range) {
17539
+ const shape = nodeInstance;
17540
+ if (!this.isRich(shape)) return false;
17541
+ let v = value;
17542
+ if (key === "link" && v !== void 0 && !isValidTextLink(String(v))) return false;
17543
+ if (key === "size" && typeof value === "number") v = sizeToRatio(value, shape.fontSize());
17544
+ const session = this.activeSession(shape);
17545
+ if (session) {
17546
+ session.setMark(key, v);
17547
+ return true;
17548
+ }
17549
+ return this.commandOnNode(shape, range, (b, s, e) => applyMark(b, s, e, key, v));
17550
+ }
17551
+ /** Link a range (or the editor selection) to an http(s) URL. */
17552
+ setRunLink(nodeInstance, url, range) {
17553
+ if (typeof url !== "string" || !isValidTextLink(url)) return false;
17554
+ return this.applyMark(nodeInstance, "link", url, range);
17555
+ }
17556
+ removeRunLink(nodeInstance, range) {
17557
+ return this.applyMark(nodeInstance, "link", void 0, range);
17558
+ }
17559
+ toggleMark(nodeInstance, key, range) {
17560
+ const shape = nodeInstance;
17561
+ if (!this.isRich(shape)) return false;
17562
+ const session = this.activeSession(shape);
17563
+ if (session) {
17564
+ session.toggleMark(key);
17565
+ return true;
17566
+ }
17567
+ return this.commandOnNode(shape, range, (b, s, e) => toggleMark(b, s, e, key));
17568
+ }
17569
+ toggleList(nodeInstance, kind, range) {
17570
+ const shape = nodeInstance;
17571
+ if (!this.isRich(shape)) return false;
17572
+ const session = this.activeSession(shape);
17573
+ if (session) {
17574
+ session.toggleList(kind);
17575
+ return true;
17576
+ }
17577
+ return this.commandOnNode(shape, range, (b, s, e) => toggleList(b, s, e, kind));
17578
+ }
17579
+ /** Align every paragraph touched by the range (default: all). */
17580
+ setAlign(nodeInstance, align, range) {
17581
+ const shape = nodeInstance;
17582
+ if (!this.isRich(shape)) return false;
17583
+ if (align !== void 0 && ![
17584
+ "left",
17585
+ "center",
17586
+ "right"
17587
+ ].includes(align)) return false;
17588
+ const session = this.activeSession(shape);
17589
+ if (session) {
17590
+ session.setAlign(align);
17591
+ return true;
17592
+ }
17593
+ return this.commandOnNode(shape, range, (b, s, e) => setBlockAlign(b, s, e, align));
17594
+ }
17595
+ indent(nodeInstance, range) {
17596
+ const shape = nodeInstance;
17597
+ if (!this.isRich(shape)) return false;
17598
+ const session = this.activeSession(shape);
17599
+ if (session) {
17600
+ session.indent();
17601
+ return true;
17602
+ }
17603
+ return this.commandOnNode(shape, range, indentBlocks);
17604
+ }
17605
+ outdent(nodeInstance, range) {
17606
+ const shape = nodeInstance;
17607
+ if (!this.isRich(shape)) return false;
17608
+ const session = this.activeSession(shape);
17609
+ if (session) {
17610
+ session.outdent();
17611
+ return true;
17612
+ }
17613
+ return this.commandOnNode(shape, range, outdentBlocks);
17614
+ }
17615
+ /**
17616
+ * Scale the whole text: sets the node `fontSize` (px); size marks are
17617
+ * ratios of it, so every run scales with it.
17618
+ */
17619
+ setBaseFontSize(nodeInstance, px) {
17620
+ const shape = nodeInstance;
17621
+ if (!this.isRich(shape)) return false;
17622
+ if (!Number.isFinite(px) || px <= 0) return false;
17623
+ if (shape.fontSize() === px) return false;
17624
+ shape.fontSize(px);
17625
+ this.applySize(shape);
17626
+ if (!this.activeSession(shape)) this.instance.updateNode(this.serialize(shape));
17627
+ this.emitFormatChange(shape, "base-size");
17628
+ return true;
17629
+ }
16481
17630
  resetSmartLayout(textNode) {
16482
17631
  textNode.setAttr("smartFixedWidth", void 0);
16483
17632
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16496,7 +17645,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16496
17645
  return next;
16497
17646
  }
16498
17647
  static getSchema() {
16499
- return WeaveTextNode.getSchema();
17648
+ const base = WeaveTextNode.getSchema();
17649
+ const marks = z.object({
17650
+ bold: z.boolean().optional(),
17651
+ italic: z.boolean().optional(),
17652
+ underline: z.boolean().optional(),
17653
+ strike: z.boolean().optional(),
17654
+ color: z.string().optional(),
17655
+ size: z.number().positive().optional().describe("Ratio of the node fontSize (1 is omitted)."),
17656
+ family: z.string().optional(),
17657
+ lineHeight: z.number().positive().optional(),
17658
+ link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional()
17659
+ }).passthrough();
17660
+ const block = z.object({
17661
+ runs: z.array(z.object({
17662
+ t: z.string(),
17663
+ m: marks.optional()
17664
+ })),
17665
+ kind: z.enum(["ul", "ol"]).optional(),
17666
+ depth: z.number().int().min(0).optional(),
17667
+ start: z.number().int().min(1).optional(),
17668
+ align: z.enum([
17669
+ "left",
17670
+ "center",
17671
+ "right"
17672
+ ]).optional()
17673
+ });
17674
+ return base.extend({ props: base.shape.props.extend({
17675
+ richText: z.boolean().optional().describe("Enables rich text commands for this node."),
17676
+ blocks: z.array(block).optional().describe("Rich content: paragraphs of runs with marks and optional list fields. `text` is the derived plain mirror.")
17677
+ }) });
16500
17678
  }
16501
17679
  };
16502
17680
  //#endregion
@@ -27040,8 +28218,16 @@ var WeaveTextToolAction = class extends WeaveAction {
27040
28218
  cancelAction;
27041
28219
  onPropsChange = void 0;
27042
28220
  onInit = void 0;
27043
- constructor() {
28221
+ toolName;
28222
+ initialProps;
28223
+ /**
28224
+ * `name` and `props` let an app register several text tools, e.g. a
28225
+ * plain one and one creating rich text nodes (`props: { richText: true }`).
28226
+ */
28227
+ constructor(params) {
27044
28228
  super();
28229
+ this.toolName = params?.name ?? "textTool";
28230
+ this.initialProps = params?.props ?? {};
27045
28231
  this.initialize();
27046
28232
  }
27047
28233
  initialize() {
@@ -27053,7 +28239,7 @@ var WeaveTextToolAction = class extends WeaveAction {
27053
28239
  this.props = this.initProps();
27054
28240
  }
27055
28241
  getName() {
27056
- return TEXT_TOOL_ACTION_NAME;
28242
+ return this.toolName;
27057
28243
  }
27058
28244
  initProps() {
27059
28245
  return {
@@ -27065,7 +28251,8 @@ var WeaveTextToolAction = class extends WeaveAction {
27065
28251
  align: "left",
27066
28252
  lineHeight: 1,
27067
28253
  verticalAlign: "top",
27068
- strokeEnabled: false
28254
+ strokeEnabled: false,
28255
+ ...this.initialProps
27069
28256
  };
27070
28257
  }
27071
28258
  setupEvents() {
@@ -35155,15 +36342,12 @@ var WeaveNodesSnappingPlugin = class extends WeavePlugin {
35155
36342
  register(instance) {
35156
36343
  super.register(instance);
35157
36344
  const hooks = this.instance.getHooks();
35158
- hooks.hook("weave:onNodeDragStart", ({ node }) => {
35159
- let containerId = "";
35160
- if (node.getAttrs().containerId) containerId = node.id();
35161
- if (node && this.snappingManagerCustomGuides.isCustomGuidesVisible(containerId)) this.snappingManagerCustomGuides.hideCustomGuides(node.id());
35162
- });
35163
- hooks.hook("weave:onNodeDragEnd", ({ node }) => {
35164
- let containerId = "";
35165
- if (node.getAttrs().containerId) containerId = node.id();
35166
- if (node && this.snappingManagerCustomGuides.isCustomGuidesVisible(containerId)) this.snappingManagerCustomGuides.renderCustomGuides(node.id());
36345
+ const guidedContainers = (nodes) => nodes.filter((node) => node.getAttrs().containerId && this.snappingManagerCustomGuides.isCustomGuidesVisible(node.id()));
36346
+ hooks.hook("weave:onTransformerDragStart", ({ nodes }) => {
36347
+ for (const node of guidedContainers(nodes)) this.snappingManagerCustomGuides.hideCustomGuides(node.id());
36348
+ });
36349
+ hooks.hook("weave:onTransformerDragEnd", ({ nodes }) => {
36350
+ for (const node of guidedContainers(nodes)) this.snappingManagerCustomGuides.renderCustomGuides(node.id());
35167
36351
  });
35168
36352
  return this;
35169
36353
  }