@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/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.596.1";
9538
+ var version = "5.7.0-SNAPSHOT.599.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) => {
@@ -14578,6 +14965,50 @@ function normalizeFontFamily(fontFamily) {
14578
14965
  function fontString(s) {
14579
14966
  return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14580
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;
15011
+ }
14581
15012
  let sharedContext = null;
14582
15013
  /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
14583
15014
  const canvasMeasurer = (font, text) => {
@@ -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;
@@ -15083,34 +15678,40 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15083
15678
  setViewport(range) {
15084
15679
  this.viewport = range;
15085
15680
  }
15086
- visibleRange() {
15681
+ /**
15682
+ * Visible y-range in local coordinates, derived from the context being
15683
+ * painted (current transform + backing canvas size) instead of the stage.
15684
+ * The stage is not the paint target during export (off-screen canvases,
15685
+ * tiles, server-side), so using it culled visible lines. Returns null (paint
15686
+ * everything) when the transform cannot be read.
15687
+ */
15688
+ visibleRange(ctx) {
15087
15689
  if (this.viewport) return this.viewport;
15088
- const stage = this.getStage();
15089
- if (!stage) return null;
15090
- const inv = this.getAbsoluteTransform().copy().invert();
15091
- const w = stage.width();
15092
- const h = stage.height();
15093
- const pts = [
15094
- inv.point({
15095
- x: 0,
15096
- y: 0
15097
- }),
15098
- inv.point({
15099
- x: w,
15100
- y: 0
15101
- }),
15102
- inv.point({
15103
- x: 0,
15104
- y: h
15105
- }),
15106
- inv.point({
15107
- x: w,
15108
- y: h
15109
- })
15110
- ];
15690
+ const native = ctx._context;
15691
+ const m = native.getTransform?.() ?? native.currentTransform;
15692
+ if (!m) return null;
15693
+ const det = m.a * m.d - m.b * m.c;
15694
+ if (!det || !Number.isFinite(det)) return null;
15695
+ const canvas = ctx.getCanvas();
15696
+ const backing = canvas._canvas;
15697
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15698
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15699
+ if (!(w > 0) || !(h > 0)) return null;
15700
+ let y1 = Infinity;
15701
+ let y2 = -Infinity;
15702
+ for (const [px, py] of [
15703
+ [0, 0],
15704
+ [w, 0],
15705
+ [0, h],
15706
+ [w, h]
15707
+ ]) {
15708
+ const y = (m.a * (py - m.f) - m.b * (px - m.e)) / det;
15709
+ if (y < y1) y1 = y;
15710
+ if (y > y2) y2 = y;
15711
+ }
15111
15712
  return {
15112
- y1: Math.min(...pts.map((p) => p.y)),
15113
- y2: Math.max(...pts.map((p) => p.y))
15713
+ y1,
15714
+ y2
15114
15715
  };
15115
15716
  }
15116
15717
  _sceneFunc = (ctx, shape) => {
@@ -15118,30 +15719,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15118
15719
  const layout = self.ensureLayout();
15119
15720
  if (layout.lines.length === 0) return;
15120
15721
  const padding = self.padding();
15121
- const lineHeightPx = layout.lineHeightPx;
15122
15722
  let baseline = "middle";
15123
- let translateY = lineHeightPx / 2;
15124
- if (!Konva.legacyTextRendering) {
15125
- const m = self.measureMetrics();
15126
- baseline = "alphabetic";
15127
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15128
- }
15723
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15129
15724
  ctx.setAttr("font", fontString(self.style()));
15130
15725
  ctx.setAttr("textBaseline", baseline);
15131
15726
  ctx.setAttr("textAlign", "left");
15132
15727
  const alignY = self.verticalOffset(layout);
15728
+ const range = self.visibleRange(ctx);
15133
15729
  ctx.translate(padding, alignY + padding);
15134
- const range = self.visibleRange();
15135
- const top = alignY + padding;
15136
- for (let n = 0; n < layout.lines.length; n++) {
15137
- const lineTop = top + n * lineHeightPx;
15138
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15139
- 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);
15140
15733
  }
15141
15734
  };
15142
15735
  /** Vertical offset of the text block inside the box (verticalAlign). */
15143
15736
  verticalOffset(layout) {
15144
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15737
+ const used = layout.contentHeight + this.padding() * 2;
15145
15738
  const va = this.verticalAlign();
15146
15739
  if (va === "middle") return (this.getHeight() - used) / 2;
15147
15740
  if (va === "bottom") return this.getHeight() - used;
@@ -15152,7 +15745,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15152
15745
  const fontSize = this.fontSize();
15153
15746
  ctx.save();
15154
15747
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15155
- 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
+ }
15156
15763
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15157
15764
  ctx.restore();
15158
15765
  }
@@ -15166,24 +15773,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15166
15773
  ctx.stroke();
15167
15774
  ctx.restore();
15168
15775
  }
15169
- 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) {
15170
15787
  const spacing = this.letterSpacing();
15171
15788
  if (spacing === 0) {
15172
- this.fillText(ctx, text, x, y);
15789
+ this.fillText(ctx, seg.t, x, y, color);
15173
15790
  return;
15174
15791
  }
15175
15792
  let cx = x;
15176
- for (const ch of Array.from(text)) {
15177
- this.fillText(ctx, ch, cx, y);
15178
- 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;
15179
15796
  }
15180
15797
  }
15181
- fillText(ctx, text, x, y) {
15798
+ fillText(ctx, text, x, y, color) {
15182
15799
  const self = this;
15183
15800
  self._partialText = text;
15184
15801
  self._partialTextX = x;
15185
15802
  self._partialTextY = y;
15186
- 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
+ }
15187
15814
  }
15188
15815
  _hitFunc = (ctx, shape) => {
15189
15816
  ctx.beginPath();
@@ -15299,9 +15926,27 @@ function lineIndexAt(layout, offset) {
15299
15926
  }
15300
15927
  return fallback;
15301
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
+ }
15302
15947
  function lineX(line, offset, measure) {
15303
15948
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15304
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15949
+ return line.offsetX + prefixWidth(line, k, measure);
15305
15950
  }
15306
15951
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15307
15952
  function caretRect(layout, offset, measure) {
@@ -15315,16 +15960,22 @@ function caretRect(layout, offset, measure) {
15315
15960
  };
15316
15961
  return {
15317
15962
  x: lineX(line, offset, measure),
15318
- y: i * layout.lineHeightPx,
15319
- height: layout.lineHeightPx,
15963
+ y: line.y,
15964
+ height: line.height,
15320
15965
  line: i
15321
15966
  };
15322
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
+ }
15323
15974
  /** Offset closest to a point in text-local coordinates. */
15324
15975
  function offsetFromPoint(layout, point, measure) {
15325
15976
  const lines = layout.lines;
15326
15977
  if (lines.length === 0) return 0;
15327
- 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);
15328
15979
  }
15329
15980
  function offsetInLine(line, x, measure) {
15330
15981
  const rel = x - line.offsetX;
@@ -15332,7 +15983,7 @@ function offsetInLine(line, x, measure) {
15332
15983
  const b = boundaries(line.text, graphemeSeg);
15333
15984
  let prevW = 0;
15334
15985
  for (let k = 1; k < b.length; k++) {
15335
- const w = measure(line.text.slice(0, b[k]));
15986
+ const w = prefixWidth(line, b[k], measure);
15336
15987
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15337
15988
  prevW = w;
15338
15989
  }
@@ -15356,7 +16007,7 @@ function selectionRects(layout, from, to, measure) {
15356
16007
  if (from > to) [from, to] = [to, from];
15357
16008
  if (from === to) return [];
15358
16009
  const out = [];
15359
- layout.lines.forEach((line, i) => {
16010
+ layout.lines.forEach((line) => {
15360
16011
  const hasNewline = line.lastInParagraph;
15361
16012
  const lineFrom = Math.max(from, line.start);
15362
16013
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15367,9 +16018,9 @@ function selectionRects(layout, from, to, measure) {
15367
16018
  if (x2 <= x1) return;
15368
16019
  out.push({
15369
16020
  x: x1,
15370
- y: i * layout.lineHeightPx,
16021
+ y: line.y,
15371
16022
  width: x2 - x1,
15372
- height: layout.lineHeightPx
16023
+ height: line.height
15373
16024
  });
15374
16025
  });
15375
16026
  return out;
@@ -15388,6 +16039,11 @@ var TextEditModel = class {
15388
16039
  focus = 0;
15389
16040
  /** x kept while moving vertically; reset by any other move. */
15390
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;
15391
16047
  undoStack = [];
15392
16048
  redoStack = [];
15393
16049
  lastKind = null;
@@ -15424,12 +16080,14 @@ var TextEditModel = class {
15424
16080
  const len = this.getText().length;
15425
16081
  this.anchor = Math.max(0, Math.min(anchor, len));
15426
16082
  this.focus = Math.max(0, Math.min(focus, len));
16083
+ this.storedMarks = null;
15427
16084
  this.breakCoalescing();
15428
16085
  }
15429
16086
  moveTo(offset, extend = false, keepDesiredX = false) {
15430
16087
  const len = this.getText().length;
15431
16088
  const o = Math.max(0, Math.min(offset, len));
15432
16089
  this.focus = o;
16090
+ this.storedMarks = null;
15433
16091
  if (!extend) this.anchor = o;
15434
16092
  if (!keepDesiredX) this.desiredX = null;
15435
16093
  this.breakCoalescing();
@@ -15437,6 +16095,7 @@ var TextEditModel = class {
15437
16095
  selectAll() {
15438
16096
  this.anchor = 0;
15439
16097
  this.focus = this.getText().length;
16098
+ this.storedMarks = null;
15440
16099
  this.breakCoalescing();
15441
16100
  }
15442
16101
  breakCoalescing() {
@@ -15472,7 +16131,14 @@ var TextEditModel = class {
15472
16131
  end = Math.max(start, Math.min(end, len));
15473
16132
  if (start === end && text === "") return false;
15474
16133
  this.record(kind, start);
15475
- 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
+ }
15476
16142
  this.anchor = this.focus = start + text.length;
15477
16143
  this.lastEditEnd = this.focus;
15478
16144
  this.desiredX = null;
@@ -15512,14 +16178,102 @@ var TextEditModel = class {
15512
16178
  this.restore(next);
15513
16179
  return true;
15514
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
+ }
15515
16261
  restore(s) {
15516
16262
  this.blocks = s.blocks;
16263
+ this.storedMarks = null;
15517
16264
  this.anchor = s.anchor;
15518
16265
  this.focus = s.focus;
15519
16266
  this.lastKind = null;
15520
16267
  this.desiredX = null;
15521
16268
  }
15522
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
+ }
15523
16277
  const SLICE_BUDGET_MS = 8;
15524
16278
  const BLINK_MS = 530;
15525
16279
  var TextEditSession = class {
@@ -15577,7 +16331,7 @@ var TextEditSession = class {
15577
16331
  };
15578
16332
  if (this.host.selectAllOnStart) this.model.selectAll();
15579
16333
  this.syncTextarea();
15580
- ta.focus();
16334
+ ta.focus({ preventScroll: true });
15581
16335
  this.positionTextarea();
15582
16336
  this.startBlink();
15583
16337
  this.repaint();
@@ -15606,7 +16360,7 @@ var TextEditSession = class {
15606
16360
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15607
16361
  }
15608
16362
  focus() {
15609
- this.textarea?.focus();
16363
+ this.textarea?.focus({ preventScroll: true });
15610
16364
  }
15611
16365
  createTextarea() {
15612
16366
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15655,8 +16409,12 @@ var TextEditSession = class {
15655
16409
  if (!ta) return;
15656
16410
  const c = this.caretStagePoint();
15657
16411
  const p = this.host.stageToContainer(c);
15658
- ta.style.left = `${p.x}px`;
15659
- 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`;
15660
16418
  }
15661
16419
  caretStagePoint() {
15662
16420
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15688,10 +16446,41 @@ var TextEditSession = class {
15688
16446
  if (e.key === "Escape") this.host.onRequestExit("escape");
15689
16447
  else if (isUndo) this.undo();
15690
16448
  else if (isRedo) this.redo();
16449
+ else if (this.handleRichKey(e, mod, key)) return true;
15691
16450
  else if (e.key === "Tab") return true;
15692
16451
  else return false;
15693
16452
  return true;
15694
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
+ }
15695
16484
  handleNavigationKey(e) {
15696
16485
  if (e.ctrlKey || e.metaKey) return false;
15697
16486
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15745,7 +16534,15 @@ var TextEditSession = class {
15745
16534
  while (p < max && oldText[p] === newText[p]) p++;
15746
16535
  let s = 0;
15747
16536
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15748
- 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
+ }
15749
16546
  const inputType = e.inputType ?? "";
15750
16547
  const kind = this.editKindFor(inputType);
15751
16548
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15786,6 +16583,7 @@ var TextEditSession = class {
15786
16583
  if (this.model.undo()) {
15787
16584
  this.syncTextarea();
15788
16585
  this.afterContentChange(0, true);
16586
+ this.host.onFormatChange?.("undo");
15789
16587
  }
15790
16588
  }
15791
16589
  redo() {
@@ -15793,8 +16591,42 @@ var TextEditSession = class {
15793
16591
  if (this.model.redo()) {
15794
16592
  this.syncTextarea();
15795
16593
  this.afterContentChange(0, true);
16594
+ this.host.onFormatChange?.("redo");
15796
16595
  }
15797
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
+ }
15798
16630
  toLocal(e) {
15799
16631
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15800
16632
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15809,12 +16641,26 @@ var TextEditSession = class {
15809
16641
  const { width, height } = this.target.getSize();
15810
16642
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15811
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
+ }
15812
16653
  onMouseDown = (e) => {
16654
+ if (this.isKeepTarget(e)) {
16655
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16656
+ return;
16657
+ }
15813
16658
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15814
16659
  };
15815
16660
  onPointerDown = (e) => {
15816
16661
  if (e.button !== 0 && e.pointerType === "mouse") return;
15817
16662
  if (this.textarea && e.target === this.textarea) return;
16663
+ if (this.isKeepTarget(e)) return;
15818
16664
  const local = this.toLocal(e);
15819
16665
  if (!this.isInside(local)) {
15820
16666
  e.stopPropagation();
@@ -15825,10 +16671,11 @@ var TextEditSession = class {
15825
16671
  e.preventDefault();
15826
16672
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15827
16673
  const text = this.model.getText();
15828
- if (e.detail >= 3) {
16674
+ const clicks = this.countClicks(e);
16675
+ if (clicks >= 3) {
15829
16676
  const [s, en] = paragraphRangeAt(text, offset);
15830
16677
  this.model.setSelection(s, en);
15831
- } else if (e.detail === 2) {
16678
+ } else if (clicks === 2) {
15832
16679
  const [s, en] = wordRangeAt(text, offset);
15833
16680
  this.model.setSelection(s, en);
15834
16681
  } else {
@@ -15837,8 +16684,31 @@ var TextEditSession = class {
15837
16684
  }
15838
16685
  this.syncTextarea();
15839
16686
  this.afterSelectionChange();
15840
- this.textarea?.focus();
16687
+ this.textarea?.focus({ preventScroll: true });
15841
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
+ }
15842
16712
  onPointerMove = (e) => {
15843
16713
  if (!this.dragging) return;
15844
16714
  const local = this.toLocal(e);
@@ -15850,13 +16720,14 @@ var TextEditSession = class {
15850
16720
  onPointerUp = () => {
15851
16721
  if (!this.dragging) return;
15852
16722
  this.dragging = false;
15853
- this.textarea?.focus();
16723
+ this.textarea?.focus({ preventScroll: true });
15854
16724
  };
15855
16725
  afterSelectionChange() {
15856
16726
  this.syncTextarea();
15857
16727
  this.positionTextarea();
15858
16728
  this.resetBlink();
15859
16729
  this.repaint();
16730
+ this.host.onSelectionChange?.();
15860
16731
  }
15861
16732
  afterContentChange(insertedLength, immediate = false) {
15862
16733
  this.flushPending();
@@ -15880,6 +16751,7 @@ var TextEditSession = class {
15880
16751
  applyContent() {
15881
16752
  this.target.setBlocks(this.model.getBlocks());
15882
16753
  this.host.onChange?.(this.model.getBlocks());
16754
+ this.host.onSelectionChange?.();
15883
16755
  this.positionTextarea();
15884
16756
  this.target.requestRedraw();
15885
16757
  this.repaint();
@@ -15955,15 +16827,29 @@ function host_signal(signal, abort) {
15955
16827
  //#region src/nodes/text-v2/constants.ts
15956
16828
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15957
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;
15958
16832
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15959
16833
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15960
16834
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15961
- edition: { outline: {
15962
- enabled: true,
15963
- color: "#0074ff",
15964
- width: 1,
15965
- padding: 0
15966
- } },
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
+ },
15967
16853
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15968
16854
  };
15969
16855
  //#endregion
@@ -15991,6 +16877,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15991
16877
  editingNodeId = null;
15992
16878
  session = null;
15993
16879
  sizeWarned = false;
16880
+ selectionTimer;
15994
16881
  nodeRenderedAddedRegistered = false;
15995
16882
  keyPressHandler;
15996
16883
  constructor(params) {
@@ -16064,6 +16951,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16064
16951
  linkPreviousTextDecoration
16065
16952
  }
16066
16953
  });
16954
+ text.setIndentStep(this.indentStep());
16955
+ text.setLinkStyle({
16956
+ color: this.config.link.defaultColor,
16957
+ hoverColor: this.config.link.hoverColor
16958
+ });
16067
16959
  this.setupDefaultNodeAugmentation(text);
16068
16960
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16069
16961
  text.getTransformerProperties = function() {
@@ -16159,7 +17051,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16159
17051
  this.applySize(shape);
16160
17052
  },
16161
17053
  getLayout: () => shape.ensureLayout(),
16162
- measure: (t) => shape.measureText(t),
17054
+ measure: (t, f) => shape.measureText(t, f),
16163
17055
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16164
17056
  getContentOffset: () => shape.getContentOffset(),
16165
17057
  getSize: () => ({
@@ -16210,7 +17102,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16210
17102
  y: p.y * sy
16211
17103
  };
16212
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
+ },
16213
17118
  selectAllOnStart: !create,
17119
+ richText: this.isRich(shape),
17120
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17121
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16214
17122
  caretColor: this.config.cursor.color,
16215
17123
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16216
17124
  ...this.config.edition.outline.enabled && {
@@ -16246,6 +17154,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16246
17154
  exitEditMode(shape) {
16247
17155
  const session = this.session;
16248
17156
  if (!session || !this.editing) return;
17157
+ clearTimeout(this.selectionTimer);
16249
17158
  const { changed } = session.getResult();
16250
17159
  session.destroy();
16251
17160
  this.session = null;
@@ -16308,11 +17217,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16308
17217
  text.getLayer()?.batchDraw();
16309
17218
  }
16310
17219
  const layout = text.getAttrs().layout;
16311
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16312
- text.width(text.width() * text.scaleX());
16313
- resetScale(text);
16314
- text.fontSize(text.fontSize() * text.scaleY());
16315
- }
17220
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16316
17221
  text.setAttr("shouldUpdateOnTransform", false);
16317
17222
  text.getLayer()?.batchDraw();
16318
17223
  });
@@ -16356,6 +17261,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16356
17261
  };
16357
17262
  const isLinked = Boolean(nextProps.link);
16358
17263
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17264
+ shape.setIndentStep(this.indentStep());
16359
17265
  shape.setAttrs({
16360
17266
  ...nextProps,
16361
17267
  ...this.outlineProps(),
@@ -16393,7 +17299,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16393
17299
  delete cleanedAttrs.dragBoundFunc;
16394
17300
  if (cleanedAttrs.link) {
16395
17301
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16396
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17302
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16397
17303
  }
16398
17304
  return {
16399
17305
  key: attrs.id ?? "",
@@ -16408,18 +17314,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16408
17314
  }
16409
17315
  };
16410
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
+ }
16411
17361
  setupLinkBehavior(text) {
16412
- text.handleMouseover = () => {
16413
- if (!text.getAttrs().link) return;
16414
- text.fill(this.config.link.hoverColor);
16415
- 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);
16416
17383
  };
16417
17384
  text.handleMouseout = () => {
17385
+ if (this.linkHoverText === text) {
17386
+ this.linkHoverText = null;
17387
+ this.linkHoverRefresh = null;
17388
+ }
17389
+ text.setHoveredRun(null);
16418
17390
  if (!text.getAttrs().link) return;
16419
17391
  text.fill(this.config.link.defaultColor);
16420
17392
  text.getLayer()?.batchDraw();
16421
17393
  };
16422
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17394
+ text.on("pointermove", updateHover);
17395
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16423
17396
  let linkClickTimeout;
16424
17397
  const clearLinkClickTimeout = () => {
16425
17398
  if (linkClickTimeout) {
@@ -16433,27 +17406,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16433
17406
  previousDblClick();
16434
17407
  };
16435
17408
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16436
- 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);
16437
17412
  if (ctrlOrMetaPressed) {
16438
17413
  clearLinkClickTimeout();
16439
- this.openTextLink(text);
17414
+ open();
16440
17415
  return;
16441
17416
  }
16442
17417
  if (wasSelected) {
16443
17418
  clearLinkClickTimeout();
16444
17419
  linkClickTimeout = setTimeout(() => {
16445
17420
  linkClickTimeout = void 0;
16446
- this.openTextLink(text);
17421
+ open();
16447
17422
  }, Konva.dblClickWindow);
16448
17423
  }
16449
17424
  };
16450
17425
  }
16451
- openTextLink(textNode) {
17426
+ localPointer(text) {
17427
+ return text.getRelativePointerPosition() ?? {
17428
+ x: -1,
17429
+ y: -1
17430
+ };
17431
+ }
17432
+ lastLinkOpen = 0;
17433
+ openUrl(link) {
16452
17434
  if (this.instance.isServerSide()) return;
16453
- const link = textNode.getAttrs().link;
16454
17435
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17436
+ const now = Date.now();
17437
+ if (now - this.lastLinkOpen < 500) return;
17438
+ this.lastLinkOpen = now;
16455
17439
  window.open(link, "_blank", "noopener,noreferrer");
16456
17440
  }
17441
+ openTextLink(textNode) {
17442
+ this.openUrl(textNode.getAttrs().link);
17443
+ }
16457
17444
  getLink(nodeInstance) {
16458
17445
  const link = nodeInstance.getAttrs().link;
16459
17446
  return typeof link === "string" ? link : void 0;
@@ -16473,6 +17460,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16473
17460
  delete serialized.props.linkPreviousTextDecoration;
16474
17461
  this.instance.updateNode(serialized);
16475
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
+ }
16476
17630
  resetSmartLayout(textNode) {
16477
17631
  textNode.setAttr("smartFixedWidth", void 0);
16478
17632
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16491,7 +17645,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16491
17645
  return next;
16492
17646
  }
16493
17647
  static getSchema() {
16494
- 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
+ }) });
16495
17678
  }
16496
17679
  };
16497
17680
  //#endregion
@@ -27035,8 +28218,16 @@ var WeaveTextToolAction = class extends WeaveAction {
27035
28218
  cancelAction;
27036
28219
  onPropsChange = void 0;
27037
28220
  onInit = void 0;
27038
- 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) {
27039
28228
  super();
28229
+ this.toolName = params?.name ?? "textTool";
28230
+ this.initialProps = params?.props ?? {};
27040
28231
  this.initialize();
27041
28232
  }
27042
28233
  initialize() {
@@ -27048,7 +28239,7 @@ var WeaveTextToolAction = class extends WeaveAction {
27048
28239
  this.props = this.initProps();
27049
28240
  }
27050
28241
  getName() {
27051
- return TEXT_TOOL_ACTION_NAME;
28242
+ return this.toolName;
27052
28243
  }
27053
28244
  initProps() {
27054
28245
  return {
@@ -27060,7 +28251,8 @@ var WeaveTextToolAction = class extends WeaveAction {
27060
28251
  align: "left",
27061
28252
  lineHeight: 1,
27062
28253
  verticalAlign: "top",
27063
- strokeEnabled: false
28254
+ strokeEnabled: false,
28255
+ ...this.initialProps
27064
28256
  };
27065
28257
  }
27066
28258
  setupEvents() {