@inditextech/weave-sdk 5.7.0-SNAPSHOT.596.1 → 5.7.0-SNAPSHOT.599.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.596.1";
9541
+ var version = "5.7.0-SNAPSHOT.599.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) => {
@@ -14538,6 +14925,50 @@ function normalizeFontFamily(fontFamily) {
14538
14925
  function fontString(s) {
14539
14926
  return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14540
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;
14971
+ }
14541
14972
  let sharedContext = null;
14542
14973
  /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
14543
14974
  const canvasMeasurer = (font, text) => {
@@ -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;
@@ -15043,34 +15638,40 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15043
15638
  setViewport(range) {
15044
15639
  this.viewport = range;
15045
15640
  }
15046
- visibleRange() {
15641
+ /**
15642
+ * Visible y-range in local coordinates, derived from the context being
15643
+ * painted (current transform + backing canvas size) instead of the stage.
15644
+ * The stage is not the paint target during export (off-screen canvases,
15645
+ * tiles, server-side), so using it culled visible lines. Returns null (paint
15646
+ * everything) when the transform cannot be read.
15647
+ */
15648
+ visibleRange(ctx) {
15047
15649
  if (this.viewport) return this.viewport;
15048
- const stage = this.getStage();
15049
- if (!stage) return null;
15050
- const inv = this.getAbsoluteTransform().copy().invert();
15051
- const w = stage.width();
15052
- const h = stage.height();
15053
- const pts = [
15054
- inv.point({
15055
- x: 0,
15056
- y: 0
15057
- }),
15058
- inv.point({
15059
- x: w,
15060
- y: 0
15061
- }),
15062
- inv.point({
15063
- x: 0,
15064
- y: h
15065
- }),
15066
- inv.point({
15067
- x: w,
15068
- y: h
15069
- })
15070
- ];
15650
+ const native = ctx._context;
15651
+ const m = native.getTransform?.() ?? native.currentTransform;
15652
+ if (!m) return null;
15653
+ const det = m.a * m.d - m.b * m.c;
15654
+ if (!det || !Number.isFinite(det)) return null;
15655
+ const canvas = ctx.getCanvas();
15656
+ const backing = canvas._canvas;
15657
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15658
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15659
+ if (!(w > 0) || !(h > 0)) return null;
15660
+ let y1 = Infinity;
15661
+ let y2 = -Infinity;
15662
+ for (const [px, py] of [
15663
+ [0, 0],
15664
+ [w, 0],
15665
+ [0, h],
15666
+ [w, h]
15667
+ ]) {
15668
+ const y = (m.a * (py - m.f) - m.b * (px - m.e)) / det;
15669
+ if (y < y1) y1 = y;
15670
+ if (y > y2) y2 = y;
15671
+ }
15071
15672
  return {
15072
- y1: Math.min(...pts.map((p) => p.y)),
15073
- y2: Math.max(...pts.map((p) => p.y))
15673
+ y1,
15674
+ y2
15074
15675
  };
15075
15676
  }
15076
15677
  _sceneFunc = (ctx, shape) => {
@@ -15078,30 +15679,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15078
15679
  const layout = self.ensureLayout();
15079
15680
  if (layout.lines.length === 0) return;
15080
15681
  const padding = self.padding();
15081
- const lineHeightPx = layout.lineHeightPx;
15082
15682
  let baseline = "middle";
15083
- let translateY = lineHeightPx / 2;
15084
- if (!Konva.legacyTextRendering) {
15085
- const m = self.measureMetrics();
15086
- baseline = "alphabetic";
15087
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15088
- }
15683
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15089
15684
  ctx.setAttr("font", fontString(self.style()));
15090
15685
  ctx.setAttr("textBaseline", baseline);
15091
15686
  ctx.setAttr("textAlign", "left");
15092
15687
  const alignY = self.verticalOffset(layout);
15688
+ const range = self.visibleRange(ctx);
15093
15689
  ctx.translate(padding, alignY + padding);
15094
- const range = self.visibleRange();
15095
- const top = alignY + padding;
15096
- for (let n = 0; n < layout.lines.length; n++) {
15097
- const lineTop = top + n * lineHeightPx;
15098
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15099
- 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);
15100
15693
  }
15101
15694
  };
15102
15695
  /** Vertical offset of the text block inside the box (verticalAlign). */
15103
15696
  verticalOffset(layout) {
15104
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15697
+ const used = layout.contentHeight + this.padding() * 2;
15105
15698
  const va = this.verticalAlign();
15106
15699
  if (va === "middle") return (this.getHeight() - used) / 2;
15107
15700
  if (va === "bottom") return this.getHeight() - used;
@@ -15112,7 +15705,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15112
15705
  const fontSize = this.fontSize();
15113
15706
  ctx.save();
15114
15707
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15115
- 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
+ }
15116
15723
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15117
15724
  ctx.restore();
15118
15725
  }
@@ -15126,24 +15733,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15126
15733
  ctx.stroke();
15127
15734
  ctx.restore();
15128
15735
  }
15129
- 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) {
15130
15747
  const spacing = this.letterSpacing();
15131
15748
  if (spacing === 0) {
15132
- this.fillText(ctx, text, x, y);
15749
+ this.fillText(ctx, seg.t, x, y, color);
15133
15750
  return;
15134
15751
  }
15135
15752
  let cx = x;
15136
- for (const ch of Array.from(text)) {
15137
- this.fillText(ctx, ch, cx, y);
15138
- 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;
15139
15756
  }
15140
15757
  }
15141
- fillText(ctx, text, x, y) {
15758
+ fillText(ctx, text, x, y, color) {
15142
15759
  const self = this;
15143
15760
  self._partialText = text;
15144
15761
  self._partialTextX = x;
15145
15762
  self._partialTextY = y;
15146
- 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
+ }
15147
15774
  }
15148
15775
  _hitFunc = (ctx, shape) => {
15149
15776
  ctx.beginPath();
@@ -15259,9 +15886,27 @@ function lineIndexAt(layout, offset) {
15259
15886
  }
15260
15887
  return fallback;
15261
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
+ }
15262
15907
  function lineX(line, offset, measure) {
15263
15908
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15264
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15909
+ return line.offsetX + prefixWidth(line, k, measure);
15265
15910
  }
15266
15911
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15267
15912
  function caretRect(layout, offset, measure) {
@@ -15275,16 +15920,22 @@ function caretRect(layout, offset, measure) {
15275
15920
  };
15276
15921
  return {
15277
15922
  x: lineX(line, offset, measure),
15278
- y: i * layout.lineHeightPx,
15279
- height: layout.lineHeightPx,
15923
+ y: line.y,
15924
+ height: line.height,
15280
15925
  line: i
15281
15926
  };
15282
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
+ }
15283
15934
  /** Offset closest to a point in text-local coordinates. */
15284
15935
  function offsetFromPoint(layout, point, measure) {
15285
15936
  const lines = layout.lines;
15286
15937
  if (lines.length === 0) return 0;
15287
- 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);
15288
15939
  }
15289
15940
  function offsetInLine(line, x, measure) {
15290
15941
  const rel = x - line.offsetX;
@@ -15292,7 +15943,7 @@ function offsetInLine(line, x, measure) {
15292
15943
  const b = boundaries(line.text, graphemeSeg);
15293
15944
  let prevW = 0;
15294
15945
  for (let k = 1; k < b.length; k++) {
15295
- const w = measure(line.text.slice(0, b[k]));
15946
+ const w = prefixWidth(line, b[k], measure);
15296
15947
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15297
15948
  prevW = w;
15298
15949
  }
@@ -15316,7 +15967,7 @@ function selectionRects(layout, from, to, measure) {
15316
15967
  if (from > to) [from, to] = [to, from];
15317
15968
  if (from === to) return [];
15318
15969
  const out = [];
15319
- layout.lines.forEach((line, i) => {
15970
+ layout.lines.forEach((line) => {
15320
15971
  const hasNewline = line.lastInParagraph;
15321
15972
  const lineFrom = Math.max(from, line.start);
15322
15973
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15327,9 +15978,9 @@ function selectionRects(layout, from, to, measure) {
15327
15978
  if (x2 <= x1) return;
15328
15979
  out.push({
15329
15980
  x: x1,
15330
- y: i * layout.lineHeightPx,
15981
+ y: line.y,
15331
15982
  width: x2 - x1,
15332
- height: layout.lineHeightPx
15983
+ height: line.height
15333
15984
  });
15334
15985
  });
15335
15986
  return out;
@@ -15348,6 +15999,11 @@ var TextEditModel = class {
15348
15999
  focus = 0;
15349
16000
  /** x kept while moving vertically; reset by any other move. */
15350
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;
15351
16007
  undoStack = [];
15352
16008
  redoStack = [];
15353
16009
  lastKind = null;
@@ -15384,12 +16040,14 @@ var TextEditModel = class {
15384
16040
  const len = this.getText().length;
15385
16041
  this.anchor = Math.max(0, Math.min(anchor, len));
15386
16042
  this.focus = Math.max(0, Math.min(focus, len));
16043
+ this.storedMarks = null;
15387
16044
  this.breakCoalescing();
15388
16045
  }
15389
16046
  moveTo(offset, extend = false, keepDesiredX = false) {
15390
16047
  const len = this.getText().length;
15391
16048
  const o = Math.max(0, Math.min(offset, len));
15392
16049
  this.focus = o;
16050
+ this.storedMarks = null;
15393
16051
  if (!extend) this.anchor = o;
15394
16052
  if (!keepDesiredX) this.desiredX = null;
15395
16053
  this.breakCoalescing();
@@ -15397,6 +16055,7 @@ var TextEditModel = class {
15397
16055
  selectAll() {
15398
16056
  this.anchor = 0;
15399
16057
  this.focus = this.getText().length;
16058
+ this.storedMarks = null;
15400
16059
  this.breakCoalescing();
15401
16060
  }
15402
16061
  breakCoalescing() {
@@ -15432,7 +16091,14 @@ var TextEditModel = class {
15432
16091
  end = Math.max(start, Math.min(end, len));
15433
16092
  if (start === end && text === "") return false;
15434
16093
  this.record(kind, start);
15435
- 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
+ }
15436
16102
  this.anchor = this.focus = start + text.length;
15437
16103
  this.lastEditEnd = this.focus;
15438
16104
  this.desiredX = null;
@@ -15472,14 +16138,102 @@ var TextEditModel = class {
15472
16138
  this.restore(next);
15473
16139
  return true;
15474
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
+ }
15475
16221
  restore(s) {
15476
16222
  this.blocks = s.blocks;
16223
+ this.storedMarks = null;
15477
16224
  this.anchor = s.anchor;
15478
16225
  this.focus = s.focus;
15479
16226
  this.lastKind = null;
15480
16227
  this.desiredX = null;
15481
16228
  }
15482
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
+ }
15483
16237
  const SLICE_BUDGET_MS = 8;
15484
16238
  const BLINK_MS = 530;
15485
16239
  var TextEditSession = class {
@@ -15537,7 +16291,7 @@ var TextEditSession = class {
15537
16291
  };
15538
16292
  if (this.host.selectAllOnStart) this.model.selectAll();
15539
16293
  this.syncTextarea();
15540
- ta.focus();
16294
+ ta.focus({ preventScroll: true });
15541
16295
  this.positionTextarea();
15542
16296
  this.startBlink();
15543
16297
  this.repaint();
@@ -15566,7 +16320,7 @@ var TextEditSession = class {
15566
16320
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15567
16321
  }
15568
16322
  focus() {
15569
- this.textarea?.focus();
16323
+ this.textarea?.focus({ preventScroll: true });
15570
16324
  }
15571
16325
  createTextarea() {
15572
16326
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15615,8 +16369,12 @@ var TextEditSession = class {
15615
16369
  if (!ta) return;
15616
16370
  const c = this.caretStagePoint();
15617
16371
  const p = this.host.stageToContainer(c);
15618
- ta.style.left = `${p.x}px`;
15619
- 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`;
15620
16378
  }
15621
16379
  caretStagePoint() {
15622
16380
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15648,10 +16406,41 @@ var TextEditSession = class {
15648
16406
  if (e.key === "Escape") this.host.onRequestExit("escape");
15649
16407
  else if (isUndo) this.undo();
15650
16408
  else if (isRedo) this.redo();
16409
+ else if (this.handleRichKey(e, mod, key)) return true;
15651
16410
  else if (e.key === "Tab") return true;
15652
16411
  else return false;
15653
16412
  return true;
15654
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
+ }
15655
16444
  handleNavigationKey(e) {
15656
16445
  if (e.ctrlKey || e.metaKey) return false;
15657
16446
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15705,7 +16494,15 @@ var TextEditSession = class {
15705
16494
  while (p < max && oldText[p] === newText[p]) p++;
15706
16495
  let s = 0;
15707
16496
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15708
- 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
+ }
15709
16506
  const inputType = e.inputType ?? "";
15710
16507
  const kind = this.editKindFor(inputType);
15711
16508
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15746,6 +16543,7 @@ var TextEditSession = class {
15746
16543
  if (this.model.undo()) {
15747
16544
  this.syncTextarea();
15748
16545
  this.afterContentChange(0, true);
16546
+ this.host.onFormatChange?.("undo");
15749
16547
  }
15750
16548
  }
15751
16549
  redo() {
@@ -15753,8 +16551,42 @@ var TextEditSession = class {
15753
16551
  if (this.model.redo()) {
15754
16552
  this.syncTextarea();
15755
16553
  this.afterContentChange(0, true);
16554
+ this.host.onFormatChange?.("redo");
15756
16555
  }
15757
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
+ }
15758
16590
  toLocal(e) {
15759
16591
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15760
16592
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15769,12 +16601,26 @@ var TextEditSession = class {
15769
16601
  const { width, height } = this.target.getSize();
15770
16602
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15771
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
+ }
15772
16613
  onMouseDown = (e) => {
16614
+ if (this.isKeepTarget(e)) {
16615
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16616
+ return;
16617
+ }
15773
16618
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15774
16619
  };
15775
16620
  onPointerDown = (e) => {
15776
16621
  if (e.button !== 0 && e.pointerType === "mouse") return;
15777
16622
  if (this.textarea && e.target === this.textarea) return;
16623
+ if (this.isKeepTarget(e)) return;
15778
16624
  const local = this.toLocal(e);
15779
16625
  if (!this.isInside(local)) {
15780
16626
  e.stopPropagation();
@@ -15785,10 +16631,11 @@ var TextEditSession = class {
15785
16631
  e.preventDefault();
15786
16632
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15787
16633
  const text = this.model.getText();
15788
- if (e.detail >= 3) {
16634
+ const clicks = this.countClicks(e);
16635
+ if (clicks >= 3) {
15789
16636
  const [s, en] = paragraphRangeAt(text, offset);
15790
16637
  this.model.setSelection(s, en);
15791
- } else if (e.detail === 2) {
16638
+ } else if (clicks === 2) {
15792
16639
  const [s, en] = wordRangeAt(text, offset);
15793
16640
  this.model.setSelection(s, en);
15794
16641
  } else {
@@ -15797,8 +16644,31 @@ var TextEditSession = class {
15797
16644
  }
15798
16645
  this.syncTextarea();
15799
16646
  this.afterSelectionChange();
15800
- this.textarea?.focus();
16647
+ this.textarea?.focus({ preventScroll: true });
15801
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
+ }
15802
16672
  onPointerMove = (e) => {
15803
16673
  if (!this.dragging) return;
15804
16674
  const local = this.toLocal(e);
@@ -15810,13 +16680,14 @@ var TextEditSession = class {
15810
16680
  onPointerUp = () => {
15811
16681
  if (!this.dragging) return;
15812
16682
  this.dragging = false;
15813
- this.textarea?.focus();
16683
+ this.textarea?.focus({ preventScroll: true });
15814
16684
  };
15815
16685
  afterSelectionChange() {
15816
16686
  this.syncTextarea();
15817
16687
  this.positionTextarea();
15818
16688
  this.resetBlink();
15819
16689
  this.repaint();
16690
+ this.host.onSelectionChange?.();
15820
16691
  }
15821
16692
  afterContentChange(insertedLength, immediate = false) {
15822
16693
  this.flushPending();
@@ -15840,6 +16711,7 @@ var TextEditSession = class {
15840
16711
  applyContent() {
15841
16712
  this.target.setBlocks(this.model.getBlocks());
15842
16713
  this.host.onChange?.(this.model.getBlocks());
16714
+ this.host.onSelectionChange?.();
15843
16715
  this.positionTextarea();
15844
16716
  this.target.requestRedraw();
15845
16717
  this.repaint();
@@ -15915,15 +16787,29 @@ function host_signal(signal, abort) {
15915
16787
  //#region src/nodes/text-v2/constants.ts
15916
16788
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15917
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;
15918
16792
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15919
16793
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15920
16794
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15921
- edition: { outline: {
15922
- enabled: true,
15923
- color: "#0074ff",
15924
- width: 1,
15925
- padding: 0
15926
- } },
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
+ },
15927
16813
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15928
16814
  };
15929
16815
  //#endregion
@@ -15951,6 +16837,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15951
16837
  editingNodeId = null;
15952
16838
  session = null;
15953
16839
  sizeWarned = false;
16840
+ selectionTimer;
15954
16841
  nodeRenderedAddedRegistered = false;
15955
16842
  keyPressHandler;
15956
16843
  constructor(params) {
@@ -16024,6 +16911,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16024
16911
  linkPreviousTextDecoration
16025
16912
  }
16026
16913
  });
16914
+ text.setIndentStep(this.indentStep());
16915
+ text.setLinkStyle({
16916
+ color: this.config.link.defaultColor,
16917
+ hoverColor: this.config.link.hoverColor
16918
+ });
16027
16919
  this.setupDefaultNodeAugmentation(text);
16028
16920
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16029
16921
  text.getTransformerProperties = function() {
@@ -16119,7 +17011,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16119
17011
  this.applySize(shape);
16120
17012
  },
16121
17013
  getLayout: () => shape.ensureLayout(),
16122
- measure: (t) => shape.measureText(t),
17014
+ measure: (t, f) => shape.measureText(t, f),
16123
17015
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16124
17016
  getContentOffset: () => shape.getContentOffset(),
16125
17017
  getSize: () => ({
@@ -16170,7 +17062,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16170
17062
  y: p.y * sy
16171
17063
  };
16172
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
+ },
16173
17078
  selectAllOnStart: !create,
17079
+ richText: this.isRich(shape),
17080
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17081
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16174
17082
  caretColor: this.config.cursor.color,
16175
17083
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16176
17084
  ...this.config.edition.outline.enabled && {
@@ -16206,6 +17114,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16206
17114
  exitEditMode(shape) {
16207
17115
  const session = this.session;
16208
17116
  if (!session || !this.editing) return;
17117
+ clearTimeout(this.selectionTimer);
16209
17118
  const { changed } = session.getResult();
16210
17119
  session.destroy();
16211
17120
  this.session = null;
@@ -16268,11 +17177,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16268
17177
  text.getLayer()?.batchDraw();
16269
17178
  }
16270
17179
  const layout = text.getAttrs().layout;
16271
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16272
- text.width(text.width() * text.scaleX());
16273
- resetScale(text);
16274
- text.fontSize(text.fontSize() * text.scaleY());
16275
- }
17180
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16276
17181
  text.setAttr("shouldUpdateOnTransform", false);
16277
17182
  text.getLayer()?.batchDraw();
16278
17183
  });
@@ -16316,6 +17221,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16316
17221
  };
16317
17222
  const isLinked = Boolean(nextProps.link);
16318
17223
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17224
+ shape.setIndentStep(this.indentStep());
16319
17225
  shape.setAttrs({
16320
17226
  ...nextProps,
16321
17227
  ...this.outlineProps(),
@@ -16353,7 +17259,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16353
17259
  delete cleanedAttrs.dragBoundFunc;
16354
17260
  if (cleanedAttrs.link) {
16355
17261
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16356
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17262
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16357
17263
  }
16358
17264
  return {
16359
17265
  key: attrs.id ?? "",
@@ -16368,18 +17274,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16368
17274
  }
16369
17275
  };
16370
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
+ }
16371
17321
  setupLinkBehavior(text) {
16372
- text.handleMouseover = () => {
16373
- if (!text.getAttrs().link) return;
16374
- text.fill(this.config.link.hoverColor);
16375
- 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);
16376
17343
  };
16377
17344
  text.handleMouseout = () => {
17345
+ if (this.linkHoverText === text) {
17346
+ this.linkHoverText = null;
17347
+ this.linkHoverRefresh = null;
17348
+ }
17349
+ text.setHoveredRun(null);
16378
17350
  if (!text.getAttrs().link) return;
16379
17351
  text.fill(this.config.link.defaultColor);
16380
17352
  text.getLayer()?.batchDraw();
16381
17353
  };
16382
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17354
+ text.on("pointermove", updateHover);
17355
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16383
17356
  let linkClickTimeout;
16384
17357
  const clearLinkClickTimeout = () => {
16385
17358
  if (linkClickTimeout) {
@@ -16393,27 +17366,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16393
17366
  previousDblClick();
16394
17367
  };
16395
17368
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16396
- 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);
16397
17372
  if (ctrlOrMetaPressed) {
16398
17373
  clearLinkClickTimeout();
16399
- this.openTextLink(text);
17374
+ open();
16400
17375
  return;
16401
17376
  }
16402
17377
  if (wasSelected) {
16403
17378
  clearLinkClickTimeout();
16404
17379
  linkClickTimeout = setTimeout(() => {
16405
17380
  linkClickTimeout = void 0;
16406
- this.openTextLink(text);
17381
+ open();
16407
17382
  }, Konva.dblClickWindow);
16408
17383
  }
16409
17384
  };
16410
17385
  }
16411
- openTextLink(textNode) {
17386
+ localPointer(text) {
17387
+ return text.getRelativePointerPosition() ?? {
17388
+ x: -1,
17389
+ y: -1
17390
+ };
17391
+ }
17392
+ lastLinkOpen = 0;
17393
+ openUrl(link) {
16412
17394
  if (this.instance.isServerSide()) return;
16413
- const link = textNode.getAttrs().link;
16414
17395
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17396
+ const now = Date.now();
17397
+ if (now - this.lastLinkOpen < 500) return;
17398
+ this.lastLinkOpen = now;
16415
17399
  window.open(link, "_blank", "noopener,noreferrer");
16416
17400
  }
17401
+ openTextLink(textNode) {
17402
+ this.openUrl(textNode.getAttrs().link);
17403
+ }
16417
17404
  getLink(nodeInstance) {
16418
17405
  const link = nodeInstance.getAttrs().link;
16419
17406
  return typeof link === "string" ? link : void 0;
@@ -16433,6 +17420,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16433
17420
  delete serialized.props.linkPreviousTextDecoration;
16434
17421
  this.instance.updateNode(serialized);
16435
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
+ }
16436
17590
  resetSmartLayout(textNode) {
16437
17591
  textNode.setAttr("smartFixedWidth", void 0);
16438
17592
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16451,7 +17605,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16451
17605
  return next;
16452
17606
  }
16453
17607
  static getSchema() {
16454
- 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
+ }) });
16455
17638
  }
16456
17639
  };
16457
17640
  //#endregion
@@ -26973,8 +28156,16 @@ var WeaveTextToolAction = class extends WeaveAction {
26973
28156
  cancelAction;
26974
28157
  onPropsChange = void 0;
26975
28158
  onInit = void 0;
26976
- 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) {
26977
28166
  super();
28167
+ this.toolName = params?.name ?? "textTool";
28168
+ this.initialProps = params?.props ?? {};
26978
28169
  this.initialize();
26979
28170
  }
26980
28171
  initialize() {
@@ -26986,7 +28177,7 @@ var WeaveTextToolAction = class extends WeaveAction {
26986
28177
  this.props = this.initProps();
26987
28178
  }
26988
28179
  getName() {
26989
- return TEXT_TOOL_ACTION_NAME;
28180
+ return this.toolName;
26990
28181
  }
26991
28182
  initProps() {
26992
28183
  return {
@@ -26998,7 +28189,8 @@ var WeaveTextToolAction = class extends WeaveAction {
26998
28189
  align: "left",
26999
28190
  lineHeight: 1,
27000
28191
  verticalAlign: "top",
27001
- strokeEnabled: false
28192
+ strokeEnabled: false,
28193
+ ...this.initialProps
27002
28194
  };
27003
28195
  }
27004
28196
  setupEvents() {