@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.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 {
@@ -13761,7 +13794,7 @@ var WeaveTextNode = class extends WeaveNode {
13761
13794
  delete cleanedAttrs.dragBoundFunc;
13762
13795
  if (cleanedAttrs.link) {
13763
13796
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
13764
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
13797
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
13765
13798
  }
13766
13799
  return {
13767
13800
  key: attrs.id ?? "",
@@ -14397,6 +14430,31 @@ var WeaveTextNode = class extends WeaveNode {
14397
14430
  };
14398
14431
  //#endregion
14399
14432
  //#region src/nodes/text-v2/model.ts
14433
+ const MIN_LINE_HEIGHT = .5;
14434
+ /** Valid `lineHeight` mark of a run, or undefined (use the node value). */
14435
+ function runLineHeight(m) {
14436
+ const v = m?.lineHeight;
14437
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(4, Math.max(MIN_LINE_HEIGHT, v)) : void 0;
14438
+ }
14439
+ /** Only absolute http(s) URLs are links (same rule as the legacy node). */
14440
+ function isSafeLink(value) {
14441
+ if (typeof value !== "string") return false;
14442
+ try {
14443
+ return ["http:", "https:"].includes(new URL(value).protocol);
14444
+ } catch {
14445
+ return false;
14446
+ }
14447
+ }
14448
+ /** Block fields that describe structure (not content). */
14449
+ function blockFields(block, withStart) {
14450
+ const out = {};
14451
+ if (!block) return out;
14452
+ if (block.kind) out.kind = block.kind;
14453
+ if (block.depth) out.depth = block.depth;
14454
+ if (block.align) out.align = block.align;
14455
+ if (withStart && block.start !== void 0) out.start = block.start;
14456
+ return out;
14457
+ }
14400
14458
  function isEmptyMarks(m) {
14401
14459
  return !m || Object.keys(m).length === 0;
14402
14460
  }
@@ -14477,7 +14535,7 @@ function marksAt(runs, offset) {
14477
14535
  * blocks) with plain `insert`. Marks on untouched text are preserved; the
14478
14536
  * inserted text inherits marks of the character before `start`.
14479
14537
  */
14480
- function replaceRange(blocks, start, end, insert) {
14538
+ function replaceRange(blocks, start, end, insert, insertMarks) {
14481
14539
  const total = blocksToText(blocks).length;
14482
14540
  start = Math.max(0, Math.min(start, total));
14483
14541
  end = Math.max(start, Math.min(end, total));
@@ -14486,6 +14544,7 @@ function replaceRange(blocks, start, end, insert) {
14486
14544
  const prefixBlocks = [];
14487
14545
  const suffixBlocks = [];
14488
14546
  let inheritedMarks;
14547
+ let startBlock;
14489
14548
  let pos = 0;
14490
14549
  blocks.forEach((block) => {
14491
14550
  const len = blockText(block).length;
@@ -14498,11 +14557,13 @@ function replaceRange(blocks, start, end, insert) {
14498
14557
  if (start >= bStart && start <= bEnd) {
14499
14558
  const local = start - bStart;
14500
14559
  before = sliceRuns(block.runs, 0, local);
14560
+ startBlock = block;
14501
14561
  inheritedMarks = marksAt(block.runs, local);
14502
14562
  }
14503
14563
  if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
14504
14564
  }
14505
14565
  });
14566
+ if (insertMarks !== void 0) inheritedMarks = insertMarks ?? void 0;
14506
14567
  const lines = insert.split("\n");
14507
14568
  const mk = (t) => t === "" ? [] : [{
14508
14569
  t,
@@ -14513,7 +14574,10 @@ function replaceRange(blocks, start, end, insert) {
14513
14574
  let runs = mk(line);
14514
14575
  if (idx === 0) runs = [...before, ...runs];
14515
14576
  if (idx === lines.length - 1) runs = [...runs, ...after];
14516
- middle.push({ runs: normalizeRuns(runs) });
14577
+ middle.push({
14578
+ ...blockFields(startBlock, idx === 0),
14579
+ runs: normalizeRuns(runs)
14580
+ });
14517
14581
  });
14518
14582
  return [
14519
14583
  ...prefixBlocks,
@@ -14521,8 +14585,331 @@ function replaceRange(blocks, start, end, insert) {
14521
14585
  ...suffixBlocks
14522
14586
  ];
14523
14587
  }
14588
+ function safeBase(base) {
14589
+ return Number.isFinite(base) && base > 0 ? base : 1;
14590
+ }
14591
+ /** px -> stored ratio (4 decimals). */
14592
+ function sizeToRatio(px, base) {
14593
+ return Math.round(px / safeBase(base) * 1e4) / 1e4;
14594
+ }
14595
+ /** stored ratio -> px (rounded to 0.1). Invalid ratios resolve to the base. */
14596
+ function ratioToSize(ratio, base) {
14597
+ const b = safeBase(base);
14598
+ return Math.round((typeof ratio === "number" && ratio > 0 ? ratio : 1) * b * 10) / 10;
14599
+ }
14600
+ /** Effective ratio of a run (invalid or missing -> 1). */
14601
+ function runRatio(m) {
14602
+ const r = m?.size;
14603
+ return typeof r === "number" && Number.isFinite(r) && r > 0 ? r : 1;
14604
+ }
14605
+ function normalizeMarks(m) {
14606
+ const out = {};
14607
+ for (const [k, v] of Object.entries(m)) {
14608
+ if (v === void 0 || v === false || v === null || v === "") continue;
14609
+ if (k === "size" && v === 1) continue;
14610
+ out[k] = v;
14611
+ }
14612
+ return Object.keys(out).length === 0 ? void 0 : out;
14613
+ }
14614
+ /** Normalized marks of a run, or undefined when empty. */
14615
+ function cleanMarks(m) {
14616
+ return m ? normalizeMarks(m) : void 0;
14617
+ }
14618
+ function blockSpans(blocks) {
14619
+ const out = [];
14620
+ let pos = 0;
14621
+ blocks.forEach((b, index) => {
14622
+ const len = blockText(b).length;
14623
+ out.push({
14624
+ index,
14625
+ start: pos,
14626
+ end: pos + len
14627
+ });
14628
+ pos += len + 1;
14629
+ });
14630
+ return out;
14631
+ }
14632
+ /** Indices of blocks touched by [start, end] (collapsed range -> its block). */
14633
+ function blockIndicesInRange(blocks, start, end) {
14634
+ const out = [];
14635
+ for (const s of blockSpans(blocks)) if (s.end >= start && s.start <= end) {
14636
+ if (end > start && s.start === end && s.start !== start) continue;
14637
+ out.push(s.index);
14638
+ }
14639
+ return out;
14640
+ }
14641
+ /** Marks that typing at a collapsed offset would inherit. */
14642
+ function marksAtOffset(blocks, offset) {
14643
+ for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) return cleanMarks(marksAt(blocks[s.index].runs, offset - s.start));
14644
+ }
14645
+ function mapRunsInRange(block, from, to, fn) {
14646
+ const runs = [];
14647
+ let pos = 0;
14648
+ for (const run of block.runs) {
14649
+ const rs = pos;
14650
+ const re = pos + run.t.length;
14651
+ pos = re;
14652
+ const s = Math.max(from, rs);
14653
+ const e = Math.min(to, re);
14654
+ if (s >= e) {
14655
+ runs.push(run);
14656
+ continue;
14657
+ }
14658
+ if (s > rs) runs.push({
14659
+ t: run.t.slice(0, s - rs),
14660
+ m: run.m
14661
+ });
14662
+ runs.push({
14663
+ t: run.t.slice(s - rs, e - rs),
14664
+ m: fn(run.m)
14665
+ });
14666
+ if (e < re) runs.push({
14667
+ t: run.t.slice(e - rs),
14668
+ m: run.m
14669
+ });
14670
+ }
14671
+ return {
14672
+ ...block,
14673
+ runs: normalizeRuns(runs)
14674
+ };
14675
+ }
14676
+ /** Set (or remove with `undefined`) one mark over the global range. */
14677
+ function applyMark(blocks, start, end, key, value) {
14678
+ if (start >= end) return blocks;
14679
+ if (key === "lineHeight" && value !== void 0) {
14680
+ const lh = runLineHeight({ lineHeight: value });
14681
+ if (lh === void 0) return blocks;
14682
+ value = lh;
14683
+ }
14684
+ const spans = blockSpans(blocks);
14685
+ return blocks.map((block, i) => {
14686
+ const s = spans[i];
14687
+ if (s.end <= start || s.start >= end) return block;
14688
+ return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
14689
+ ...m,
14690
+ [key]: value
14691
+ }));
14692
+ });
14693
+ }
14694
+ /** Query one mark over a range; collapsed ranges report the caret marks. */
14695
+ function queryMark(blocks, start, end, key, stored) {
14696
+ const values = [];
14697
+ if (start >= end) {
14698
+ const v = (stored ?? marksAtOffset(blocks, start))?.[key];
14699
+ return v === void 0 || v === false ? { state: "none" } : {
14700
+ state: "all",
14701
+ value: v
14702
+ };
14703
+ }
14704
+ const spans = blockSpans(blocks);
14705
+ blocks.forEach((block, i) => {
14706
+ const s = spans[i];
14707
+ if (s.end <= start || s.start >= end) return;
14708
+ const from = Math.max(start, s.start) - s.start;
14709
+ const to = Math.min(end, s.end) - s.start;
14710
+ for (const r of sliceRuns(block.runs, from, to)) {
14711
+ const v = r.m?.[key];
14712
+ values.push(v === false ? void 0 : v);
14713
+ }
14714
+ });
14715
+ if (values.length === 0) return { state: "none" };
14716
+ const set = values.filter((v) => v !== void 0);
14717
+ if (set.length === 0) return { state: "none" };
14718
+ if (set.length < values.length) return { state: "some" };
14719
+ return values.every((v) => v === values[0]) ? {
14720
+ state: "all",
14721
+ value: values[0]
14722
+ } : { state: "some" };
14723
+ }
14724
+ /** Toggle a boolean mark: removes when fully set, sets otherwise. */
14725
+ function toggleMark(blocks, start, end, key) {
14726
+ return applyMark(blocks, start, end, key, queryMark(blocks, start, end, key).state === "all" ? void 0 : true);
14727
+ }
14728
+ function mapBlocks(blocks, indices, fn) {
14729
+ const set = new Set(indices);
14730
+ return blocks.map((b, i) => set.has(i) ? fn(b) : b);
14731
+ }
14732
+ function stripList(b) {
14733
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
14734
+ return rest;
14735
+ }
14736
+ /** Toggle list `kind` on the blocks of a range (all already that kind -> off). */
14737
+ function toggleList(blocks, start, end, kind) {
14738
+ const idx = blockIndicesInRange(blocks, start, end);
14739
+ const allSame = idx.every((i) => blocks[i].kind === kind);
14740
+ return mapBlocks(blocks, idx, (b) => allSame ? stripList(b) : {
14741
+ ...b,
14742
+ kind
14743
+ });
14744
+ }
14745
+ /** Set (or clear with `undefined`) the alignment of the blocks in a range. */
14746
+ function setBlockAlign(blocks, start, end, align) {
14747
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14748
+ if (align === void 0) {
14749
+ const { align: _a, ...rest } = b;
14750
+ return rest;
14751
+ }
14752
+ return {
14753
+ ...b,
14754
+ align
14755
+ };
14756
+ });
14757
+ }
14758
+ /** Increase depth of list blocks in the range (non-list blocks untouched). */
14759
+ function indentBlocks(blocks, start, end) {
14760
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => b.kind ? {
14761
+ ...b,
14762
+ depth: (b.depth ?? 0) + 1
14763
+ } : b);
14764
+ }
14765
+ /** Decrease depth; at depth 0 the list marker is removed. */
14766
+ function outdentBlocks(blocks, start, end) {
14767
+ return mapBlocks(blocks, blockIndicesInRange(blocks, start, end), (b) => {
14768
+ if (!b.kind) return b;
14769
+ const d = b.depth ?? 0;
14770
+ if (d <= 0) return stripList(b);
14771
+ const next = {
14772
+ ...b,
14773
+ depth: d - 1
14774
+ };
14775
+ if (d - 1 === 0) delete next.depth;
14776
+ return next;
14777
+ });
14778
+ }
14779
+ const BULLETS = [
14780
+ "•",
14781
+ "◦",
14782
+ "▪"
14783
+ ];
14784
+ function alpha(n) {
14785
+ let s = "";
14786
+ let v = n;
14787
+ while (v > 0) {
14788
+ v--;
14789
+ s = String.fromCharCode(97 + v % 26) + s;
14790
+ v = Math.floor(v / 26);
14791
+ }
14792
+ return s;
14793
+ }
14794
+ function roman(n) {
14795
+ const map = [
14796
+ [1e3, "m"],
14797
+ [900, "cm"],
14798
+ [500, "d"],
14799
+ [400, "cd"],
14800
+ [100, "c"],
14801
+ [90, "xc"],
14802
+ [50, "l"],
14803
+ [40, "xl"],
14804
+ [10, "x"],
14805
+ [9, "ix"],
14806
+ [5, "v"],
14807
+ [4, "iv"],
14808
+ [1, "i"]
14809
+ ];
14810
+ let v = n;
14811
+ let s = "";
14812
+ for (const [k, r] of map) while (v >= k) {
14813
+ s += r;
14814
+ v -= k;
14815
+ }
14816
+ return s;
14817
+ }
14818
+ /**
14819
+ * Marker text per block (null for plain paragraphs). Ordered numbering is
14820
+ * continuous across consecutive same-depth items and restarts after a
14821
+ * non-list block, a shallower item or a kind change; `start` seeds a run.
14822
+ * Markers are layout-only: they never enter `text` or offsets.
14823
+ */
14824
+ function listMarkers(blocks) {
14825
+ const stack = [];
14826
+ return blocks.map((b) => {
14827
+ if (!b.kind) {
14828
+ stack.length = 0;
14829
+ return null;
14830
+ }
14831
+ const d = Math.max(0, Math.floor(b.depth ?? 0));
14832
+ stack.length = Math.min(stack.length, d + 1);
14833
+ while (stack.length < d) stack.push({
14834
+ kind: b.kind,
14835
+ n: 0
14836
+ });
14837
+ const cur = stack[d];
14838
+ if (!cur || cur.kind !== b.kind) stack[d] = {
14839
+ kind: b.kind,
14840
+ n: b.kind === "ol" ? Math.max(1, b.start ?? 1) : 0
14841
+ };
14842
+ else if (b.kind === "ol") cur.n += 1;
14843
+ if (b.kind === "ul") return BULLETS[d % 3];
14844
+ const n = stack[d].n;
14845
+ const style = d % 3;
14846
+ return `${style === 0 ? n : style === 1 ? alpha(n) : roman(n)}.`;
14847
+ });
14848
+ }
14849
+ /**
14850
+ * Format of the range for toolbars. A collapsed range reports the stored
14851
+ * marks (when given) or the marks typing would inherit.
14852
+ */
14853
+ function getSelectionFormat(blocks, start, end, baseFontSize, stored) {
14854
+ const q = (key) => queryMark(blocks, start, end, key, stored);
14855
+ const ratios = [];
14856
+ if (start >= end) ratios.push(runRatio(stored ?? marksAtOffset(blocks, start)));
14857
+ else {
14858
+ const spans = blockSpans(blocks);
14859
+ blocks.forEach((block, i) => {
14860
+ const s = spans[i];
14861
+ if (s.end <= start || s.start >= end) return;
14862
+ const from = Math.max(start, s.start) - s.start;
14863
+ const to = Math.min(end, s.end) - s.start;
14864
+ for (const r of sliceRuns(block.runs, from, to)) ratios.push(runRatio(r.m));
14865
+ });
14866
+ }
14867
+ let size;
14868
+ if (ratios.length === 0 || ratios.every((r) => r === ratios[0])) {
14869
+ const r = ratios[0] ?? 1;
14870
+ size = {
14871
+ state: r === 1 ? "none" : "all",
14872
+ value: ratioToSize(r, baseFontSize)
14873
+ };
14874
+ } else size = { state: "some" };
14875
+ const idx = blockIndicesInRange(blocks, start, end);
14876
+ const kinds = new Set(idx.map((i) => blocks[i].kind ?? "none"));
14877
+ let list = { kind: "none" };
14878
+ if (kinds.size === 1) {
14879
+ const kind = [...kinds][0];
14880
+ const depths = new Set(idx.map((i) => blocks[i].depth ?? 0));
14881
+ list = kind === "none" ? { kind } : {
14882
+ kind,
14883
+ ...depths.size === 1 ? { depth: [...depths][0] } : {}
14884
+ };
14885
+ } else if (kinds.size > 1) list = { kind: "mixed" };
14886
+ return {
14887
+ bold: q("bold"),
14888
+ italic: q("italic"),
14889
+ underline: q("underline"),
14890
+ strike: q("strike"),
14891
+ color: q("color"),
14892
+ family: q("family"),
14893
+ link: q("link"),
14894
+ lineHeight: q("lineHeight"),
14895
+ align: alignQuery(blocks, start, end),
14896
+ size,
14897
+ list
14898
+ };
14899
+ }
14900
+ function alignQuery(blocks, start, end) {
14901
+ const vals = blockIndicesInRange(blocks, start, end).map((i) => blocks[i].align);
14902
+ const set = vals.filter((v) => v !== void 0);
14903
+ if (set.length === 0) return { state: "none" };
14904
+ if (set.length < vals.length || set.some((v) => v !== set[0])) return { state: "some" };
14905
+ return {
14906
+ state: "all",
14907
+ value: set[0]
14908
+ };
14909
+ }
14524
14910
  //#endregion
14525
14911
  //#region src/nodes/text-v2/layout/engine.ts
14912
+ const DEFAULT_INDENT_STEP = 1.5;
14526
14913
  /** Quote multi-word families like `Konva` does so the font shorthand parses. */
14527
14914
  function normalizeFontFamily(fontFamily) {
14528
14915
  return fontFamily.split(",").map((family) => {
@@ -14535,6 +14922,50 @@ function normalizeFontFamily(fontFamily) {
14535
14922
  function fontString(s) {
14536
14923
  return `${s.fontStyle} ${s.fontVariant} ${s.fontSize}px ${normalizeFontFamily(s.fontFamily)}`;
14537
14924
  }
14925
+ /** Style of a run: node style overridden by the run marks. */
14926
+ function runStyle(style, m) {
14927
+ if (!m) return style;
14928
+ let fontStyle = style.fontStyle;
14929
+ if (m.bold || m.italic) {
14930
+ const tokens = new Set(fontStyle.split(/\s+/).filter(Boolean));
14931
+ tokens.delete("normal");
14932
+ if (m.italic) tokens.add("italic");
14933
+ if (m.bold) tokens.add("bold");
14934
+ fontStyle = tokens.size === 0 ? "normal" : Array.from(tokens).join(" ");
14935
+ }
14936
+ return {
14937
+ ...style,
14938
+ fontStyle,
14939
+ fontSize: style.fontSize * runRatio(m),
14940
+ fontFamily: typeof m.family === "string" && m.family ? m.family : style.fontFamily
14941
+ };
14942
+ }
14943
+ /** Pure fallback (same constants as Konva.Text when the browser has none). */
14944
+ const approxMetrics = (_font, size) => ({
14945
+ ascent: .91 * size,
14946
+ descent: .21 * size
14947
+ });
14948
+ const metricsCache = /* @__PURE__ */ new Map();
14949
+ /** Ascent/descent used for baseline placement (parity with Konva.Text). */
14950
+ function fontMetrics(font, size) {
14951
+ if (Konva.legacyTextRendering) return {
14952
+ ascent: 0,
14953
+ descent: 0
14954
+ };
14955
+ const hit = metricsCache.get(font);
14956
+ if (hit) return hit;
14957
+ const ctx = Konva.Util.createCanvasElement().getContext("2d");
14958
+ ctx.font = font;
14959
+ const m = ctx.measureText("M");
14960
+ const scale = size / 100;
14961
+ const out = {
14962
+ ascent: m.fontBoundingBoxAscent ?? 91 * scale,
14963
+ descent: m.fontBoundingBoxDescent ?? 21 * scale
14964
+ };
14965
+ if (metricsCache.size > 2e3) metricsCache.clear();
14966
+ metricsCache.set(font, out);
14967
+ return out;
14968
+ }
14538
14969
  let sharedContext = null;
14539
14970
  /** Default measurer: works on browser, node-canvas and skia-canvas backends. */
14540
14971
  const canvasMeasurer = (font, text) => {
@@ -14582,11 +15013,15 @@ function wordWrapIndex(graphemes, start, matchLen, matchWidth, maxWidth) {
14582
15013
  const seg = graphemes.slice(start, start + matchLen);
14583
15014
  return Math.max(seg.lastIndexOf(" "), seg.lastIndexOf("-")) + 1;
14584
15015
  }
14585
- function applyAlign(lines, align, width, padding) {
14586
- if (align === "left") return;
15016
+ function applyAlign(lines, width, padding) {
14587
15017
  for (const line of lines) {
14588
- const free = width - line.width - padding * 2;
14589
- line.offsetX = align === "center" ? free / 2 : free;
15018
+ const align = line.align;
15019
+ if (align === "left") {
15020
+ line.offsetX = line.indent;
15021
+ continue;
15022
+ }
15023
+ const free = width - line.indent - line.width - padding * 2;
15024
+ line.offsetX = line.indent + (align === "center" ? free / 2 : free);
14590
15025
  }
14591
15026
  }
14592
15027
  var TextLayoutEngine = class {
@@ -14596,8 +15031,10 @@ var TextLayoutEngine = class {
14596
15031
  /** Entries by signature, so paragraphs that shifted index are reused. */
14597
15032
  pool = /* @__PURE__ */ new Map();
14598
15033
  lastParagraphsRelaid = 0;
14599
- constructor(measurer = canvasMeasurer) {
15034
+ metrics;
15035
+ constructor(measurer = canvasMeasurer, metrics = approxMetrics) {
14600
15036
  this.measure = measurer;
15037
+ this.metrics = metrics;
14601
15038
  }
14602
15039
  /** Number of paragraphs recomputed by the last `layout()` call. */
14603
15040
  getLastRelaidCount() {
@@ -14608,10 +15045,17 @@ var TextLayoutEngine = class {
14608
15045
  this.paragraphs = [];
14609
15046
  this.pool.clear();
14610
15047
  }
14611
- paragraphSig(block, style, bounds) {
14612
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
14613
- const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14614
- return `${fontString(style)}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}|${JSON.stringify(block.runs)}`;
15048
+ /** Text indent of a block in px (0 for plain paragraphs). */
15049
+ indentFor(block, style, fixedWidth, maxWidth) {
15050
+ if (!block.kind) return 0;
15051
+ const stepPx = (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15052
+ const wanted = (Math.max(0, Math.floor(block.depth ?? 0)) + 1) * stepPx;
15053
+ if (!fixedWidth) return wanted;
15054
+ const limit = Math.max(0, maxWidth - 4 * style.fontSize);
15055
+ return Math.min(wanted, limit);
15056
+ }
15057
+ sigFor(block, style, fixedWidth, maxWidth, indent) {
15058
+ return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
14615
15059
  }
14616
15060
  /**
14617
15061
  * Pre-compute paragraph layouts for `blocks` within a time budget so a
@@ -14619,14 +15063,14 @@ var TextLayoutEngine = class {
14619
15063
  * Lets callers time-slice very large content (e.g. bulk paste).
14620
15064
  */
14621
15065
  warm(blocks, style, bounds, budgetMs) {
14622
- const font = fontString(style);
14623
- const fixedWidth = bounds.width !== void 0 && !Number.isNaN(bounds.width);
15066
+ const fixedWidth = isNum(bounds.width);
14624
15067
  const maxWidth = (bounds.width ?? 0) - bounds.padding * 2;
14625
15068
  const deadline = now() + budgetMs;
14626
15069
  for (const block of blocks) {
14627
- const sig = this.paragraphSig(block, style, bounds);
15070
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15071
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14628
15072
  if (this.pool.has(sig)) continue;
14629
- this.pool.set(sig, this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth));
15073
+ this.pool.set(sig, this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent));
14630
15074
  if (now() >= deadline) return false;
14631
15075
  }
14632
15076
  return true;
@@ -14644,35 +15088,38 @@ var TextLayoutEngine = class {
14644
15088
  return w + text.length * spacing;
14645
15089
  }
14646
15090
  layout(blocks, style, bounds) {
14647
- const font = fontString(style);
14648
15091
  const padding = bounds.padding;
14649
15092
  const fixedWidth = isNum(bounds.width);
14650
15093
  const fixedHeight = isNum(bounds.height);
14651
15094
  const maxWidth = (bounds.width ?? 0) - padding * 2;
14652
15095
  const maxHeightPx = (bounds.height ?? 0) - padding * 2;
14653
15096
  const lineHeightPx = style.lineHeight * style.fontSize;
14654
- const sigBase = `${font}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth : "auto"}`;
15097
+ const markers = listMarkers(blocks);
14655
15098
  const prev = this.paragraphs;
14656
15099
  const next = [];
14657
15100
  let relaid = 0;
14658
15101
  const lines = [];
14659
15102
  let textWidth = 0;
14660
15103
  let offset = 0;
15104
+ let contentHeight = 0;
14661
15105
  for (let p = 0; p < blocks.length; p++) {
14662
15106
  const block = blocks[p];
14663
- const sig = `${sigBase}|${JSON.stringify(block.runs)}`;
15107
+ const indent = this.indentFor(block, style, fixedWidth, maxWidth);
15108
+ const sig = this.sigFor(block, style, fixedWidth, maxWidth, indent);
14664
15109
  let entry = prev[p]?.sig === sig ? prev[p] : this.pool.get(sig);
14665
15110
  if (!entry) {
14666
- entry = this.layoutParagraph(block, sig, font, style, fixedWidth, maxWidth);
15111
+ entry = this.layoutParagraph(block, sig, style, fixedWidth, maxWidth - indent);
14667
15112
  relaid++;
14668
15113
  }
14669
15114
  next.push(entry);
14670
15115
  const emitted = this.emitLines(entry, block, p, offset, lines, {
14671
- font,
14672
15116
  style,
14673
- lineHeightPx,
15117
+ indent,
15118
+ marker: markers[p],
15119
+ y: contentHeight,
14674
15120
  maxHeightPx: fixedHeight ? maxHeightPx : void 0
14675
15121
  });
15122
+ contentHeight = emitted.y;
14676
15123
  textWidth = Math.max(textWidth, emitted.maxWidth);
14677
15124
  if (emitted.stopped) break;
14678
15125
  offset += blockText(block).length + 1;
@@ -14681,25 +15128,42 @@ var TextLayoutEngine = class {
14681
15128
  this.pool = new Map(next.map((e) => [e.sig, e]));
14682
15129
  this.lastParagraphsRelaid = relaid;
14683
15130
  const width = fixedWidth ? bounds.width : textWidth + padding * 2;
14684
- const height = fixedHeight ? bounds.height : lines.length * lineHeightPx + padding * 2;
14685
- applyAlign(lines, style.align, width, padding);
15131
+ const height = fixedHeight ? bounds.height : contentHeight + padding * 2;
15132
+ applyAlign(lines, width, padding);
14686
15133
  return {
14687
15134
  lines,
14688
15135
  textWidth,
14689
15136
  textHeight: style.fontSize,
14690
15137
  lineHeightPx,
15138
+ contentHeight,
14691
15139
  width,
14692
15140
  height
14693
15141
  };
14694
15142
  }
14695
15143
  /** Append a paragraph's lines to `lines`; stops once a fixed height is full. */
14696
15144
  emitLines(entry, block, paragraph, offset, lines, opts) {
14697
- const { font, style, lineHeightPx, maxHeightPx } = opts;
15145
+ const { style, indent, maxHeightPx } = opts;
15146
+ let { y } = opts;
14698
15147
  let maxWidth = 0;
15148
+ let first = true;
14699
15149
  for (const pl of entry.lines) {
14700
15150
  const text = entry.graphemes.slice(pl.startG, pl.endG).join("");
14701
- const segments = this.buildSegments(entry, block, pl, font, style);
15151
+ const segments = this.buildSegments(entry, block, pl, style);
14702
15152
  const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
15153
+ let marker;
15154
+ if (first && opts.marker) {
15155
+ const m0 = block.runs[0]?.m;
15156
+ const rs = runStyle(style, m0);
15157
+ const stepPx = Math.min(indent, this.stepPx(style));
15158
+ marker = {
15159
+ text: opts.marker,
15160
+ x: Math.max(0, indent - stepPx),
15161
+ font: fontString(rs),
15162
+ fontSize: rs.fontSize,
15163
+ color: typeof m0?.color === "string" ? m0.color : void 0
15164
+ };
15165
+ }
15166
+ first = false;
14703
15167
  lines.push({
14704
15168
  paragraph,
14705
15169
  start: offset + startChar,
@@ -14707,30 +15171,41 @@ var TextLayoutEngine = class {
14707
15171
  text,
14708
15172
  width: segments.reduce((a, s) => a + s.width, 0),
14709
15173
  measuredWidth: pl.measuredWidth,
14710
- offsetX: 0,
14711
- y: lines.length * lineHeightPx,
15174
+ offsetX: indent,
15175
+ y,
15176
+ height: pl.height,
15177
+ baseline: pl.baseline,
15178
+ indent,
15179
+ marker,
15180
+ align: block.align ?? style.align,
14712
15181
  segments,
14713
15182
  lastInParagraph: pl.lastInParagraph
14714
15183
  });
14715
- maxWidth = Math.max(maxWidth, pl.measuredWidth);
14716
- if (maxHeightPx !== void 0 && (lines.length + 1) * lineHeightPx > maxHeightPx) return {
15184
+ y += pl.height;
15185
+ maxWidth = Math.max(maxWidth, pl.measuredWidth + indent);
15186
+ if (maxHeightPx !== void 0 && y + pl.height > maxHeightPx) return {
14717
15187
  stopped: true,
14718
- maxWidth
15188
+ maxWidth,
15189
+ y
14719
15190
  };
14720
15191
  }
14721
15192
  return {
14722
15193
  stopped: false,
14723
- maxWidth
15194
+ maxWidth,
15195
+ y
14724
15196
  };
14725
15197
  }
14726
- rangeWidth(entry, from, to, font, spacing) {
15198
+ stepPx(style) {
15199
+ return (typeof style.indentStep === "number" && Number.isFinite(style.indentStep) && style.indentStep > 0 ? style.indentStep : DEFAULT_INDENT_STEP) * style.fontSize;
15200
+ }
15201
+ rangeWidth(entry, from, to, spacing) {
14727
15202
  let width = 0;
14728
15203
  let i = from;
14729
15204
  while (i < to) {
14730
15205
  const run = entry.runOf[i];
14731
15206
  let j = i;
14732
15207
  while (j < to && entry.runOf[j] === run) j++;
14733
- width += this.measureChunk(font, entry.graphemes.slice(i, j).join(""), spacing);
15208
+ width += this.measureChunk(entry.fonts[run], entry.graphemes.slice(i, j).join(""), spacing);
14734
15209
  i = j;
14735
15210
  }
14736
15211
  return width;
@@ -14766,17 +15241,58 @@ var TextLayoutEngine = class {
14766
15241
  matchWidth
14767
15242
  };
14768
15243
  }
14769
- layoutParagraph(block, sig, font, style, fixedWidth, maxWidth) {
15244
+ layoutParagraph(block, sig, style, fixedWidth, maxWidth) {
15245
+ const entry = this.wrapParagraph(block, sig, style, fixedWidth, maxWidth);
15246
+ this.measureLines(entry, style);
15247
+ return entry;
15248
+ }
15249
+ /** Per-line box height and baseline from the runs on each line. */
15250
+ measureLines(entry, style) {
15251
+ const baseFont = fontString(style);
15252
+ for (const line of entry.lines) {
15253
+ let size = 0;
15254
+ let height = 0;
15255
+ let asc = 0;
15256
+ let desc = 0;
15257
+ const seen = /* @__PURE__ */ new Set();
15258
+ for (let g = line.startG; g < line.endG; g++) {
15259
+ const r = entry.runOf[g];
15260
+ if (seen.has(r)) continue;
15261
+ seen.add(r);
15262
+ size = Math.max(size, entry.sizes[r]);
15263
+ height = Math.max(height, entry.lineHeights[r] * entry.sizes[r]);
15264
+ const m = this.metrics(entry.fonts[r], entry.sizes[r]);
15265
+ asc = Math.max(asc, m.ascent);
15266
+ desc = Math.max(desc, m.descent);
15267
+ }
15268
+ if (seen.size === 0) {
15269
+ size = style.fontSize;
15270
+ height = style.lineHeight * size;
15271
+ const m = this.metrics(baseFont, style.fontSize);
15272
+ asc = m.ascent;
15273
+ desc = m.descent;
15274
+ }
15275
+ line.height = height;
15276
+ line.baseline = (asc - desc) / 2 + line.height / 2;
15277
+ }
15278
+ }
15279
+ wrapParagraph(block, sig, style, fixedWidth, maxWidth) {
14770
15280
  const { graphemes, runOf } = flattenBlock(block);
15281
+ const baseFont = fontString(style);
15282
+ const fonts = block.runs.map((r) => r.m ? fontString(runStyle(style, r.m)) : baseFont);
15283
+ const sizes = block.runs.map((r) => style.fontSize * runRatio(r.m));
14771
15284
  const entry = {
14772
15285
  sig,
14773
15286
  graphemes,
14774
15287
  runOf,
15288
+ fonts,
15289
+ lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
15290
+ sizes,
14775
15291
  lines: []
14776
15292
  };
14777
15293
  const n = graphemes.length;
14778
15294
  const spacing = style.letterSpacing;
14779
- const width = (a, b) => this.rangeWidth(entry, a, b, font, spacing);
15295
+ const width = (a, b) => this.rangeWidth(entry, a, b, spacing);
14780
15296
  const shouldWrap = style.wrap !== "none";
14781
15297
  const wrapAtWord = style.wrap !== "char" && shouldWrap;
14782
15298
  const whole = width(0, n);
@@ -14785,7 +15301,9 @@ var TextLayoutEngine = class {
14785
15301
  startG: 0,
14786
15302
  endG: n,
14787
15303
  measuredWidth: whole,
14788
- lastInParagraph: true
15304
+ lastInParagraph: true,
15305
+ height: 0,
15306
+ baseline: 0
14789
15307
  });
14790
15308
  return entry;
14791
15309
  }
@@ -14797,7 +15315,9 @@ var TextLayoutEngine = class {
14797
15315
  startG: start,
14798
15316
  endG: n,
14799
15317
  measuredWidth: matchWidth,
14800
- lastInParagraph: true
15318
+ lastInParagraph: true,
15319
+ height: 0,
15320
+ baseline: 0
14801
15321
  });
14802
15322
  return entry;
14803
15323
  }
@@ -14813,7 +15333,9 @@ var TextLayoutEngine = class {
14813
15333
  startG: start,
14814
15334
  endG: trimTrailingSpace(graphemes, start, end),
14815
15335
  measuredWidth: matchWidth,
14816
- lastInParagraph: false
15336
+ lastInParagraph: false,
15337
+ height: 0,
15338
+ baseline: 0
14817
15339
  });
14818
15340
  start = skipSpace(graphemes, end);
14819
15341
  }
@@ -14821,7 +15343,7 @@ var TextLayoutEngine = class {
14821
15343
  if (last) last.lastInParagraph = true;
14822
15344
  return entry;
14823
15345
  }
14824
- buildSegments(entry, block, pl, font, style) {
15346
+ buildSegments(entry, block, pl, style) {
14825
15347
  const segments = [];
14826
15348
  let x = 0;
14827
15349
  let i = pl.startG;
@@ -14830,12 +15352,16 @@ var TextLayoutEngine = class {
14830
15352
  let j = i;
14831
15353
  while (j < pl.endG && entry.runOf[j] === run) j++;
14832
15354
  const t = entry.graphemes.slice(i, j).join("");
15355
+ const font = entry.fonts[run];
14833
15356
  const width = this.measureChunk(font, t, style.letterSpacing);
14834
15357
  segments.push({
14835
15358
  t,
14836
15359
  m: block.runs[run].m,
15360
+ run,
14837
15361
  x,
14838
- width
15362
+ width,
15363
+ font,
15364
+ fontSize: entry.sizes[run]
14839
15365
  });
14840
15366
  x += width;
14841
15367
  i = j;
@@ -14867,12 +15393,18 @@ const LAYOUT_ATTRS = [
14867
15393
  * code written against `Konva.Text` keeps working.
14868
15394
  */
14869
15395
  var WeaveTextV2Shape = class extends Konva.Shape {
14870
- engine = new TextLayoutEngine();
14871
- naturalEngine = new TextLayoutEngine();
15396
+ engine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
15397
+ naturalEngine = new TextLayoutEngine(canvasMeasurer, fontMetrics);
14872
15398
  layoutCache = null;
14873
15399
  blocksKey = {};
14874
15400
  blocksCache = [];
14875
15401
  viewport = null;
15402
+ indentStepValue;
15403
+ linkStyle = {
15404
+ color: "#1155ccff",
15405
+ hoverColor: "#3d7be0ff"
15406
+ };
15407
+ hoveredRun = null;
14876
15408
  constructor(config) {
14877
15409
  super(config);
14878
15410
  this.className = "WeaveTextV2Shape";
@@ -14889,6 +15421,55 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14889
15421
  this._setAttr("text", value == null ? "" : String(value));
14890
15422
  return this;
14891
15423
  }
15424
+ /**
15425
+ * List indent per depth, as a multiple of `fontSize`. Not a node attr: it
15426
+ * comes from the node config and must not be serialized into the document.
15427
+ */
15428
+ setIndentStep(step) {
15429
+ if (this.indentStepValue !== step) {
15430
+ this.indentStepValue = step;
15431
+ this.layoutCache = null;
15432
+ }
15433
+ return this;
15434
+ }
15435
+ /** Link run colours (from the node config; not serialized). */
15436
+ setLinkStyle(style) {
15437
+ this.linkStyle = style;
15438
+ return this;
15439
+ }
15440
+ /** Run under the pointer (`paragraph:run`), repainted with the hover colour. */
15441
+ setHoveredRun(key) {
15442
+ if (this.hoveredRun !== key) {
15443
+ this.hoveredRun = key;
15444
+ this.getLayer()?.batchDraw();
15445
+ }
15446
+ return this;
15447
+ }
15448
+ getHoveredRun() {
15449
+ return this.hoveredRun;
15450
+ }
15451
+ /**
15452
+ * Link run at a point in node-local coordinates (the legacy node-level
15453
+ * `link` wins: runs are then not links).
15454
+ */
15455
+ linkAtPoint(point) {
15456
+ if (this.attrs.link) return null;
15457
+ const layout = this.ensureLayout();
15458
+ const o = this.getContentOffset();
15459
+ const x = point.x - o.x;
15460
+ const y = point.y - o.y;
15461
+ for (const line of layout.lines) {
15462
+ if (y < line.y || y >= line.y + line.height) continue;
15463
+ for (const seg of line.segments) {
15464
+ const sx = line.offsetX + seg.x;
15465
+ if (x >= sx && x <= sx + seg.width && isSafeLink(seg.m?.link)) return {
15466
+ key: `${line.paragraph}:${seg.run}`,
15467
+ url: seg.m.link
15468
+ };
15469
+ }
15470
+ }
15471
+ return null;
15472
+ }
14892
15473
  /** Resolved blocks: stored or lazily synthesized (never written back). */
14893
15474
  getBlocks() {
14894
15475
  const k = this.blocksKey;
@@ -14909,6 +15490,10 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14909
15490
  }
14910
15491
  /** Replace content with blocks, keeping the `text` mirror in sync. */
14911
15492
  setBlocks(blocks) {
15493
+ if (!Array.isArray(blocks)) {
15494
+ this._setAttr("blocks", void 0);
15495
+ return this;
15496
+ }
14912
15497
  this._setAttr("blocks", blocks);
14913
15498
  this._setAttr("text", blocksToText(blocks));
14914
15499
  return this;
@@ -14946,6 +15531,16 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14946
15531
  textDecoration(v) {
14947
15532
  return this.accessor("textDecoration", "", v);
14948
15533
  }
15534
+ /**
15535
+ * Always stroke under the node transform. Weave turns `strokeScaleEnabled`
15536
+ * off while a node is transformed; Konva then resets the canvas transform
15537
+ * before stroking, which only works for path shapes. `strokeText` is not a
15538
+ * path, so the outline was painted at local coordinates on the canvas
15539
+ * origin at stage scale 1 (visible over other nodes while resizing).
15540
+ */
15541
+ getStrokeScaleEnabled() {
15542
+ return true;
15543
+ }
14949
15544
  accessor(name, fallback, value) {
14950
15545
  if (value === void 0) return this.attrs[name] ?? fallback;
14951
15546
  this.setAttr(name, value);
@@ -14980,7 +15575,8 @@ var WeaveTextV2Shape = class extends Konva.Shape {
14980
15575
  lineHeight: this.lineHeight(),
14981
15576
  letterSpacing: this.letterSpacing(),
14982
15577
  wrap: this.wrap(),
14983
- align: this.align()
15578
+ align: this.align(),
15579
+ indentStep: this.indentStepValue
14984
15580
  };
14985
15581
  }
14986
15582
  /** Lazy, cached layout. Size accessors and painting force it. */
@@ -15010,14 +15606,13 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15010
15606
  };
15011
15607
  }
15012
15608
  /** Rendered width of a string with this shape's font and letter spacing. */
15013
- measureText(text) {
15014
- return canvasMeasurer(fontString(this.style()), text) + text.length * this.letterSpacing();
15609
+ measureText(text, font) {
15610
+ return canvasMeasurer(font ?? fontString(this.style()), text) + text.length * this.letterSpacing();
15015
15611
  }
15016
15612
  /** Offset of the first line inside the box (padding + vertical align). */
15017
15613
  getContentOffset() {
15018
15614
  const padding = this.padding();
15019
- const layout = this.ensureLayout();
15020
- const used = layout.lines.length * layout.lineHeightPx + padding * 2;
15615
+ const used = this.ensureLayout().contentHeight + padding * 2;
15021
15616
  const va = this.verticalAlign();
15022
15617
  let alignY = 0;
15023
15618
  if (va === "middle") alignY = (this.getHeight() - used) / 2;
@@ -15040,34 +15635,40 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15040
15635
  setViewport(range) {
15041
15636
  this.viewport = range;
15042
15637
  }
15043
- visibleRange() {
15638
+ /**
15639
+ * Visible y-range in local coordinates, derived from the context being
15640
+ * painted (current transform + backing canvas size) instead of the stage.
15641
+ * The stage is not the paint target during export (off-screen canvases,
15642
+ * tiles, server-side), so using it culled visible lines. Returns null (paint
15643
+ * everything) when the transform cannot be read.
15644
+ */
15645
+ visibleRange(ctx) {
15044
15646
  if (this.viewport) return this.viewport;
15045
- const stage = this.getStage();
15046
- if (!stage) return null;
15047
- const inv = this.getAbsoluteTransform().copy().invert();
15048
- const w = stage.width();
15049
- const h = stage.height();
15050
- const pts = [
15051
- inv.point({
15052
- x: 0,
15053
- y: 0
15054
- }),
15055
- inv.point({
15056
- x: w,
15057
- y: 0
15058
- }),
15059
- inv.point({
15060
- x: 0,
15061
- y: h
15062
- }),
15063
- inv.point({
15064
- x: w,
15065
- y: h
15066
- })
15067
- ];
15647
+ const native = ctx._context;
15648
+ const m = native.getTransform?.() ?? native.currentTransform;
15649
+ if (!m) return null;
15650
+ const det = m.a * m.d - m.b * m.c;
15651
+ if (!det || !Number.isFinite(det)) return null;
15652
+ const canvas = ctx.getCanvas();
15653
+ const backing = canvas._canvas;
15654
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15655
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15656
+ if (!(w > 0) || !(h > 0)) return null;
15657
+ let y1 = Infinity;
15658
+ let y2 = -Infinity;
15659
+ for (const [px, py] of [
15660
+ [0, 0],
15661
+ [w, 0],
15662
+ [0, h],
15663
+ [w, h]
15664
+ ]) {
15665
+ const y = (m.a * (py - m.f) - m.b * (px - m.e)) / det;
15666
+ if (y < y1) y1 = y;
15667
+ if (y > y2) y2 = y;
15668
+ }
15068
15669
  return {
15069
- y1: Math.min(...pts.map((p) => p.y)),
15070
- y2: Math.max(...pts.map((p) => p.y))
15670
+ y1,
15671
+ y2
15071
15672
  };
15072
15673
  }
15073
15674
  _sceneFunc = (ctx, shape) => {
@@ -15075,30 +15676,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15075
15676
  const layout = self.ensureLayout();
15076
15677
  if (layout.lines.length === 0) return;
15077
15678
  const padding = self.padding();
15078
- const lineHeightPx = layout.lineHeightPx;
15079
15679
  let baseline = "middle";
15080
- let translateY = lineHeightPx / 2;
15081
- if (!Konva.legacyTextRendering) {
15082
- const m = self.measureMetrics();
15083
- baseline = "alphabetic";
15084
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15085
- }
15680
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15086
15681
  ctx.setAttr("font", fontString(self.style()));
15087
15682
  ctx.setAttr("textBaseline", baseline);
15088
15683
  ctx.setAttr("textAlign", "left");
15089
15684
  const alignY = self.verticalOffset(layout);
15685
+ const range = self.visibleRange(ctx);
15090
15686
  ctx.translate(padding, alignY + padding);
15091
- const range = self.visibleRange();
15092
- const top = alignY + padding;
15093
- for (let n = 0; n < layout.lines.length; n++) {
15094
- const lineTop = top + n * lineHeightPx;
15095
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15096
- self.paintLine(ctx, layout.lines[n], translateY + n * lineHeightPx);
15687
+ for (const line of layout.lines) {
15688
+ if (range && (alignY + padding + line.y + line.height < range.y1 - line.height || alignY + padding + line.y > range.y2 + line.height)) continue;
15689
+ self.paintLine(ctx, line, line.y + line.baseline);
15097
15690
  }
15098
15691
  };
15099
15692
  /** Vertical offset of the text block inside the box (verticalAlign). */
15100
15693
  verticalOffset(layout) {
15101
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15694
+ const used = layout.contentHeight + this.padding() * 2;
15102
15695
  const va = this.verticalAlign();
15103
15696
  if (va === "middle") return (this.getHeight() - used) / 2;
15104
15697
  if (va === "bottom") return this.getHeight() - used;
@@ -15109,7 +15702,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15109
15702
  const fontSize = this.fontSize();
15110
15703
  ctx.save();
15111
15704
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15112
- for (const seg of line.segments) this.paintSegment(ctx, seg.t, line.offsetX + seg.x, y);
15705
+ if (line.marker) {
15706
+ const mk = line.marker;
15707
+ ctx.setAttr("font", mk.font);
15708
+ this.fillText(ctx, mk.text, mk.x, y, mk.color);
15709
+ }
15710
+ for (const seg of line.segments) {
15711
+ ctx.setAttr("font", seg.font);
15712
+ const x = line.offsetX + seg.x;
15713
+ const isLink = !this.attrs.link && isSafeLink(seg.m?.link);
15714
+ let color = typeof seg.m?.color === "string" ? seg.m.color : void 0;
15715
+ if (isLink) color = this.hoveredRun === `${line.paragraph}:${seg.run}` ? this.linkStyle.hoverColor : this.linkStyle.color;
15716
+ this.paintSegment(ctx, seg, x, y, color);
15717
+ if (seg.m?.underline || isLink) this.strokeSegment(ctx, seg, x, y + Math.round(seg.fontSize / 4), color);
15718
+ if (seg.m?.strike) this.strokeSegment(ctx, seg, x, y - Math.round(seg.fontSize / 4), color);
15719
+ }
15113
15720
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15114
15721
  ctx.restore();
15115
15722
  }
@@ -15123,24 +15730,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15123
15730
  ctx.stroke();
15124
15731
  ctx.restore();
15125
15732
  }
15126
- paintSegment(ctx, text, x, y) {
15733
+ strokeSegment(ctx, seg, x, y, color) {
15734
+ ctx.save();
15735
+ ctx.beginPath();
15736
+ ctx.moveTo(x, y);
15737
+ ctx.lineTo(x + Math.round(seg.width), y);
15738
+ ctx.lineWidth = seg.fontSize / 15;
15739
+ ctx.strokeStyle = color ?? this.fill();
15740
+ ctx.stroke();
15741
+ ctx.restore();
15742
+ }
15743
+ paintSegment(ctx, seg, x, y, color) {
15127
15744
  const spacing = this.letterSpacing();
15128
15745
  if (spacing === 0) {
15129
- this.fillText(ctx, text, x, y);
15746
+ this.fillText(ctx, seg.t, x, y, color);
15130
15747
  return;
15131
15748
  }
15132
15749
  let cx = x;
15133
- for (const ch of Array.from(text)) {
15134
- this.fillText(ctx, ch, cx, y);
15135
- cx += this.measureSize(ch).width + spacing;
15750
+ for (const ch of Array.from(seg.t)) {
15751
+ this.fillText(ctx, ch, cx, y, color);
15752
+ cx += canvasMeasurer(seg.font, ch) + spacing;
15136
15753
  }
15137
15754
  }
15138
- fillText(ctx, text, x, y) {
15755
+ fillText(ctx, text, x, y, color) {
15139
15756
  const self = this;
15140
15757
  self._partialText = text;
15141
15758
  self._partialTextX = x;
15142
15759
  self._partialTextY = y;
15143
- ctx.fillStrokeShape(this);
15760
+ if (color === void 0) {
15761
+ ctx.fillStrokeShape(this);
15762
+ return;
15763
+ }
15764
+ const prev = this.attrs.fill;
15765
+ this.attrs.fill = color;
15766
+ try {
15767
+ ctx.fillStrokeShape(this);
15768
+ } finally {
15769
+ this.attrs.fill = prev;
15770
+ }
15144
15771
  }
15145
15772
  _hitFunc = (ctx, shape) => {
15146
15773
  ctx.beginPath();
@@ -15256,9 +15883,27 @@ function lineIndexAt(layout, offset) {
15256
15883
  }
15257
15884
  return fallback;
15258
15885
  }
15886
+ /** Width of the first `k` UTF-16 units of a line, run by run. */
15887
+ function prefixWidth(line, k, measure) {
15888
+ if (k <= 0) return 0;
15889
+ if (line.segments.length === 0) return measure(line.text.slice(0, k));
15890
+ let width = 0;
15891
+ let pos = 0;
15892
+ for (const seg of line.segments) {
15893
+ const end = pos + seg.t.length;
15894
+ if (k >= end) width += seg.width;
15895
+ else {
15896
+ width += measure(seg.t.slice(0, k - pos), seg.font);
15897
+ break;
15898
+ }
15899
+ pos = end;
15900
+ if (k === end) break;
15901
+ }
15902
+ return width;
15903
+ }
15259
15904
  function lineX(line, offset, measure) {
15260
15905
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15261
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15906
+ return line.offsetX + prefixWidth(line, k, measure);
15262
15907
  }
15263
15908
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15264
15909
  function caretRect(layout, offset, measure) {
@@ -15272,16 +15917,22 @@ function caretRect(layout, offset, measure) {
15272
15917
  };
15273
15918
  return {
15274
15919
  x: lineX(line, offset, measure),
15275
- y: i * layout.lineHeightPx,
15276
- height: layout.lineHeightPx,
15920
+ y: line.y,
15921
+ height: line.height,
15277
15922
  line: i
15278
15923
  };
15279
15924
  }
15925
+ /** Index of the line containing a local y (clamped). */
15926
+ function lineIndexAtY(layout, y) {
15927
+ const lines = layout.lines;
15928
+ for (let i = 0; i < lines.length; i++) if (y < lines[i].y + lines[i].height) return i;
15929
+ return Math.max(0, lines.length - 1);
15930
+ }
15280
15931
  /** Offset closest to a point in text-local coordinates. */
15281
15932
  function offsetFromPoint(layout, point, measure) {
15282
15933
  const lines = layout.lines;
15283
15934
  if (lines.length === 0) return 0;
15284
- return offsetInLine(lines[Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / layout.lineHeightPx)))], point.x, measure);
15935
+ return offsetInLine(lines[lineIndexAtY(layout, point.y)], point.x, measure);
15285
15936
  }
15286
15937
  function offsetInLine(line, x, measure) {
15287
15938
  const rel = x - line.offsetX;
@@ -15289,7 +15940,7 @@ function offsetInLine(line, x, measure) {
15289
15940
  const b = boundaries(line.text, graphemeSeg);
15290
15941
  let prevW = 0;
15291
15942
  for (let k = 1; k < b.length; k++) {
15292
- const w = measure(line.text.slice(0, b[k]));
15943
+ const w = prefixWidth(line, b[k], measure);
15293
15944
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15294
15945
  prevW = w;
15295
15946
  }
@@ -15313,7 +15964,7 @@ function selectionRects(layout, from, to, measure) {
15313
15964
  if (from > to) [from, to] = [to, from];
15314
15965
  if (from === to) return [];
15315
15966
  const out = [];
15316
- layout.lines.forEach((line, i) => {
15967
+ layout.lines.forEach((line) => {
15317
15968
  const hasNewline = line.lastInParagraph;
15318
15969
  const lineFrom = Math.max(from, line.start);
15319
15970
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15324,9 +15975,9 @@ function selectionRects(layout, from, to, measure) {
15324
15975
  if (x2 <= x1) return;
15325
15976
  out.push({
15326
15977
  x: x1,
15327
- y: i * layout.lineHeightPx,
15978
+ y: line.y,
15328
15979
  width: x2 - x1,
15329
- height: layout.lineHeightPx
15980
+ height: line.height
15330
15981
  });
15331
15982
  });
15332
15983
  return out;
@@ -15345,6 +15996,11 @@ var TextEditModel = class {
15345
15996
  focus = 0;
15346
15997
  /** x kept while moving vertically; reset by any other move. */
15347
15998
  desiredX = null;
15999
+ /**
16000
+ * Marks the next typed text takes at a collapsed caret (e.g. bold toggled
16001
+ * with nothing selected). Cleared when the selection moves.
16002
+ */
16003
+ storedMarks = null;
15348
16004
  undoStack = [];
15349
16005
  redoStack = [];
15350
16006
  lastKind = null;
@@ -15381,12 +16037,14 @@ var TextEditModel = class {
15381
16037
  const len = this.getText().length;
15382
16038
  this.anchor = Math.max(0, Math.min(anchor, len));
15383
16039
  this.focus = Math.max(0, Math.min(focus, len));
16040
+ this.storedMarks = null;
15384
16041
  this.breakCoalescing();
15385
16042
  }
15386
16043
  moveTo(offset, extend = false, keepDesiredX = false) {
15387
16044
  const len = this.getText().length;
15388
16045
  const o = Math.max(0, Math.min(offset, len));
15389
16046
  this.focus = o;
16047
+ this.storedMarks = null;
15390
16048
  if (!extend) this.anchor = o;
15391
16049
  if (!keepDesiredX) this.desiredX = null;
15392
16050
  this.breakCoalescing();
@@ -15394,6 +16052,7 @@ var TextEditModel = class {
15394
16052
  selectAll() {
15395
16053
  this.anchor = 0;
15396
16054
  this.focus = this.getText().length;
16055
+ this.storedMarks = null;
15397
16056
  this.breakCoalescing();
15398
16057
  }
15399
16058
  breakCoalescing() {
@@ -15429,7 +16088,14 @@ var TextEditModel = class {
15429
16088
  end = Math.max(start, Math.min(end, len));
15430
16089
  if (start === end && text === "") return false;
15431
16090
  this.record(kind, start);
15432
- this.blocks = replaceRange(this.blocks, start, end, text);
16091
+ const carried = this.storedMarks ?? marksAtOffset(this.blocks, start);
16092
+ this.blocks = replaceRange(this.blocks, start, end, text, this.storedMarks ?? void 0);
16093
+ this.storedMarks = null;
16094
+ if (text.endsWith("\n") && carried) {
16095
+ const { link: _link, ...rest } = carried;
16096
+ const next = cleanMarks(rest);
16097
+ if (next && !marksAtOffset(this.blocks, start + text.length)) this.storedMarks = next;
16098
+ }
15433
16099
  this.anchor = this.focus = start + text.length;
15434
16100
  this.lastEditEnd = this.focus;
15435
16101
  this.desiredX = null;
@@ -15469,14 +16135,102 @@ var TextEditModel = class {
15469
16135
  this.restore(next);
15470
16136
  return true;
15471
16137
  }
16138
+ /** Current marks at the caret / selection start (stored marks win). */
16139
+ caretMarks() {
16140
+ return this.storedMarks ?? marksAtOffset(this.blocks, this.getSelection().start);
16141
+ }
16142
+ /** Always one new undo entry, never coalesced. */
16143
+ commitFormat(next) {
16144
+ if (JSON.stringify(next) === JSON.stringify(this.blocks)) return false;
16145
+ this.lastKind = null;
16146
+ this.record("format", -1);
16147
+ this.lastKind = null;
16148
+ this.blocks = next;
16149
+ return true;
16150
+ }
16151
+ /** Set (`undefined` clears) one mark on the selection or the stored marks. */
16152
+ setMark(key, value) {
16153
+ const { start, end } = this.getSelection();
16154
+ if (start === end) {
16155
+ const base = { ...this.caretMarks() ?? {} };
16156
+ if (value === void 0 || value === false) delete base[key];
16157
+ else base[key] = value;
16158
+ this.storedMarks = cleanMarks(base) ?? {};
16159
+ return true;
16160
+ }
16161
+ return this.commitFormat(applyMark(this.blocks, start, end, key, value));
16162
+ }
16163
+ /** Toggle a boolean mark: on unless the whole selection already has it. */
16164
+ toggleMark(key) {
16165
+ const { start, end } = this.getSelection();
16166
+ const q = queryMark(this.blocks, start, end, key, this.storedMarks);
16167
+ return this.setMark(key, q.state === "all" ? void 0 : true);
16168
+ }
16169
+ toggleList(kind) {
16170
+ const { start, end } = this.getSelection();
16171
+ return this.commitFormat(toggleList(this.blocks, start, end, kind));
16172
+ }
16173
+ setAlign(align) {
16174
+ const { start, end } = this.getSelection();
16175
+ return this.commitFormat(setBlockAlign(this.blocks, start, end, align));
16176
+ }
16177
+ indent() {
16178
+ const { start, end } = this.getSelection();
16179
+ return this.commitFormat(indentBlocks(this.blocks, start, end));
16180
+ }
16181
+ outdent() {
16182
+ const { start, end } = this.getSelection();
16183
+ return this.commitFormat(outdentBlocks(this.blocks, start, end));
16184
+ }
16185
+ blockAt(offset) {
16186
+ let pos = 0;
16187
+ for (let i = 0; i < this.blocks.length; i++) {
16188
+ const len = blockText(this.blocks[i]).length;
16189
+ if (offset <= pos + len) return {
16190
+ index: i,
16191
+ start: pos,
16192
+ length: len
16193
+ };
16194
+ pos += len + 1;
16195
+ }
16196
+ }
16197
+ /**
16198
+ * Enter in a list: an empty item exits the list; otherwise the item
16199
+ * continues (the block split carries `kind` and `depth`).
16200
+ */
16201
+ enter() {
16202
+ const { start, end } = this.getSelection();
16203
+ const b = start === end ? this.blockAt(start) : void 0;
16204
+ if (b && this.blocks[b.index].kind && b.length === 0) return this.commitFormat(outdentToPlain(this.blocks, b.index));
16205
+ return this.replaceRange(start, end, "\n", "other");
16206
+ }
16207
+ /**
16208
+ * Backspace at the start of a list item outdents it, or removes the
16209
+ * marker at depth 0. Returns false when the key is not handled here.
16210
+ */
16211
+ backspaceInList() {
16212
+ const { start, end } = this.getSelection();
16213
+ if (start !== end) return false;
16214
+ const b = this.blockAt(start);
16215
+ if (!b || b.start !== start || !this.blocks[b.index].kind) return false;
16216
+ return this.commitFormat(outdentBlocks(this.blocks, start, start));
16217
+ }
15472
16218
  restore(s) {
15473
16219
  this.blocks = s.blocks;
16220
+ this.storedMarks = null;
15474
16221
  this.anchor = s.anchor;
15475
16222
  this.focus = s.focus;
15476
16223
  this.lastKind = null;
15477
16224
  this.desiredX = null;
15478
16225
  }
15479
16226
  };
16227
+ function outdentToPlain(blocks, index) {
16228
+ return blocks.map((b, i) => {
16229
+ if (i !== index) return b;
16230
+ const { kind: _k, depth: _d, start: _s, ...rest } = b;
16231
+ return rest;
16232
+ });
16233
+ }
15480
16234
  const SLICE_BUDGET_MS = 8;
15481
16235
  const BLINK_MS = 530;
15482
16236
  var TextEditSession = class {
@@ -15534,7 +16288,7 @@ var TextEditSession = class {
15534
16288
  };
15535
16289
  if (this.host.selectAllOnStart) this.model.selectAll();
15536
16290
  this.syncTextarea();
15537
- ta.focus();
16291
+ ta.focus({ preventScroll: true });
15538
16292
  this.positionTextarea();
15539
16293
  this.startBlink();
15540
16294
  this.repaint();
@@ -15563,7 +16317,7 @@ var TextEditSession = class {
15563
16317
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15564
16318
  }
15565
16319
  focus() {
15566
- this.textarea?.focus();
16320
+ this.textarea?.focus({ preventScroll: true });
15567
16321
  }
15568
16322
  createTextarea() {
15569
16323
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15612,8 +16366,12 @@ var TextEditSession = class {
15612
16366
  if (!ta) return;
15613
16367
  const c = this.caretStagePoint();
15614
16368
  const p = this.host.stageToContainer(c);
15615
- ta.style.left = `${p.x}px`;
15616
- ta.style.top = `${p.y}px`;
16369
+ this.host.revealCaret?.(p);
16370
+ const box = this.host.container;
16371
+ const maxX = Math.max(0, (box.clientWidth || Infinity) - 2);
16372
+ const maxY = Math.max(0, (box.clientHeight || Infinity) - 20);
16373
+ ta.style.left = `${Math.min(Math.max(p.x, 0), maxX)}px`;
16374
+ ta.style.top = `${Math.min(Math.max(p.y, 0), maxY)}px`;
15617
16375
  }
15618
16376
  caretStagePoint() {
15619
16377
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15645,10 +16403,41 @@ var TextEditSession = class {
15645
16403
  if (e.key === "Escape") this.host.onRequestExit("escape");
15646
16404
  else if (isUndo) this.undo();
15647
16405
  else if (isRedo) this.redo();
16406
+ else if (this.handleRichKey(e, mod, key)) return true;
15648
16407
  else if (e.key === "Tab") return true;
15649
16408
  else return false;
15650
16409
  return true;
15651
16410
  }
16411
+ /** Format shortcuts and list key rules; only when rich text is enabled. */
16412
+ handleRichKey(e, mod, key) {
16413
+ if (!this.host.richText) return false;
16414
+ if (mod && !e.shiftKey && !e.altKey) {
16415
+ const marks = {
16416
+ b: "bold",
16417
+ i: "italic",
16418
+ u: "underline"
16419
+ };
16420
+ if (marks[key]) {
16421
+ this.toggleMark(marks[key]);
16422
+ return true;
16423
+ }
16424
+ }
16425
+ if (mod || this.composing) return false;
16426
+ if (e.key === "Tab") {
16427
+ if (e.shiftKey) this.outdent();
16428
+ else this.indent();
16429
+ return true;
16430
+ }
16431
+ if (e.key === "Enter") {
16432
+ if (this.model.enter()) this.afterFormat("command", true);
16433
+ return true;
16434
+ }
16435
+ if (e.key === "Backspace" && this.model.backspaceInList()) {
16436
+ this.afterFormat("command", true);
16437
+ return true;
16438
+ }
16439
+ return false;
16440
+ }
15652
16441
  handleNavigationKey(e) {
15653
16442
  if (e.ctrlKey || e.metaKey) return false;
15654
16443
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15702,7 +16491,15 @@ var TextEditSession = class {
15702
16491
  while (p < max && oldText[p] === newText[p]) p++;
15703
16492
  let s = 0;
15704
16493
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15705
- const inserted = newText.slice(p, newText.length - s);
16494
+ let inserted = newText.slice(p, newText.length - s);
16495
+ if (s + p === oldText.length && inserted !== "") {
16496
+ const q = ta.selectionEnd - inserted.length;
16497
+ if (q >= 0 && q < p && newText.slice(q, q + inserted.length) === inserted && oldText.slice(0, q) + inserted + oldText.slice(q) === newText) {
16498
+ s = oldText.length - q;
16499
+ p = q;
16500
+ inserted = newText.slice(p, newText.length - s);
16501
+ }
16502
+ }
15706
16503
  const inputType = e.inputType ?? "";
15707
16504
  const kind = this.editKindFor(inputType);
15708
16505
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15743,6 +16540,7 @@ var TextEditSession = class {
15743
16540
  if (this.model.undo()) {
15744
16541
  this.syncTextarea();
15745
16542
  this.afterContentChange(0, true);
16543
+ this.host.onFormatChange?.("undo");
15746
16544
  }
15747
16545
  }
15748
16546
  redo() {
@@ -15750,8 +16548,42 @@ var TextEditSession = class {
15750
16548
  if (this.model.redo()) {
15751
16549
  this.syncTextarea();
15752
16550
  this.afterContentChange(0, true);
16551
+ this.host.onFormatChange?.("redo");
15753
16552
  }
15754
16553
  }
16554
+ afterFormat(reason, content = false) {
16555
+ this.flushPending();
16556
+ this.syncTextarea();
16557
+ if (content) this.afterContentChange(0, true);
16558
+ else this.applyContent();
16559
+ this.afterSelectionChange();
16560
+ this.host.onFormatChange?.(reason);
16561
+ }
16562
+ toggleMark(key) {
16563
+ if (this.model.toggleMark(key)) this.afterFormat("command");
16564
+ }
16565
+ setMark(key, value) {
16566
+ if (this.model.setMark(key, value)) this.afterFormat("command");
16567
+ }
16568
+ toggleList(kind) {
16569
+ if (this.model.toggleList(kind)) this.afterFormat("command");
16570
+ }
16571
+ setAlign(align) {
16572
+ if (this.model.setAlign(align)) this.afterFormat("command");
16573
+ }
16574
+ indent() {
16575
+ if (this.model.indent()) this.afterFormat("command");
16576
+ }
16577
+ outdent() {
16578
+ if (this.model.outdent()) this.afterFormat("command");
16579
+ }
16580
+ getSelection() {
16581
+ return this.model.getSelection();
16582
+ }
16583
+ getFormat() {
16584
+ const { start, end } = this.model.getSelection();
16585
+ return getSelectionFormat(this.model.getBlocks(), start, end, this.target.getFontSize(), this.model.storedMarks);
16586
+ }
15755
16587
  toLocal(e) {
15756
16588
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15757
16589
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15766,12 +16598,26 @@ var TextEditSession = class {
15766
16598
  const { width, height } = this.target.getSize();
15767
16599
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15768
16600
  }
16601
+ /**
16602
+ * True for pointer targets inside an element the app marked with
16603
+ * `data-weave-text-edit-keep` (e.g. a formatting toolbar): they neither
16604
+ * end the session nor, unless they are form fields, steal focus.
16605
+ */
16606
+ isKeepTarget(e) {
16607
+ const t = e.target;
16608
+ return typeof Element !== "undefined" && t instanceof Element && !!t.closest("[data-weave-text-edit-keep]");
16609
+ }
15769
16610
  onMouseDown = (e) => {
16611
+ if (this.isKeepTarget(e)) {
16612
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16613
+ return;
16614
+ }
15770
16615
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15771
16616
  };
15772
16617
  onPointerDown = (e) => {
15773
16618
  if (e.button !== 0 && e.pointerType === "mouse") return;
15774
16619
  if (this.textarea && e.target === this.textarea) return;
16620
+ if (this.isKeepTarget(e)) return;
15775
16621
  const local = this.toLocal(e);
15776
16622
  if (!this.isInside(local)) {
15777
16623
  e.stopPropagation();
@@ -15782,10 +16628,11 @@ var TextEditSession = class {
15782
16628
  e.preventDefault();
15783
16629
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15784
16630
  const text = this.model.getText();
15785
- if (e.detail >= 3) {
16631
+ const clicks = this.countClicks(e);
16632
+ if (clicks >= 3) {
15786
16633
  const [s, en] = paragraphRangeAt(text, offset);
15787
16634
  this.model.setSelection(s, en);
15788
- } else if (e.detail === 2) {
16635
+ } else if (clicks === 2) {
15789
16636
  const [s, en] = wordRangeAt(text, offset);
15790
16637
  this.model.setSelection(s, en);
15791
16638
  } else {
@@ -15794,8 +16641,31 @@ var TextEditSession = class {
15794
16641
  }
15795
16642
  this.syncTextarea();
15796
16643
  this.afterSelectionChange();
15797
- this.textarea?.focus();
16644
+ this.textarea?.focus({ preventScroll: true });
15798
16645
  };
16646
+ lastClick = {
16647
+ t: 0,
16648
+ x: 0,
16649
+ y: 0,
16650
+ n: 0
16651
+ };
16652
+ /**
16653
+ * Browsers report `detail` as 0 on pointerdown, so multi-click is counted
16654
+ * here (same spot, within 500 ms). A real `detail` still wins.
16655
+ */
16656
+ countClicks(e) {
16657
+ const now = e.timeStamp || Date.now();
16658
+ const l = this.lastClick;
16659
+ const near = now - l.t < 500 && Math.hypot(e.clientX - l.x, e.clientY - l.y) < 5 && l.n > 0;
16660
+ const n = Math.max(e.detail, near ? l.n + 1 : 1);
16661
+ this.lastClick = {
16662
+ t: now,
16663
+ x: e.clientX,
16664
+ y: e.clientY,
16665
+ n: n >= 3 ? 0 : n
16666
+ };
16667
+ return n;
16668
+ }
15799
16669
  onPointerMove = (e) => {
15800
16670
  if (!this.dragging) return;
15801
16671
  const local = this.toLocal(e);
@@ -15807,13 +16677,14 @@ var TextEditSession = class {
15807
16677
  onPointerUp = () => {
15808
16678
  if (!this.dragging) return;
15809
16679
  this.dragging = false;
15810
- this.textarea?.focus();
16680
+ this.textarea?.focus({ preventScroll: true });
15811
16681
  };
15812
16682
  afterSelectionChange() {
15813
16683
  this.syncTextarea();
15814
16684
  this.positionTextarea();
15815
16685
  this.resetBlink();
15816
16686
  this.repaint();
16687
+ this.host.onSelectionChange?.();
15817
16688
  }
15818
16689
  afterContentChange(insertedLength, immediate = false) {
15819
16690
  this.flushPending();
@@ -15837,6 +16708,7 @@ var TextEditSession = class {
15837
16708
  applyContent() {
15838
16709
  this.target.setBlocks(this.model.getBlocks());
15839
16710
  this.host.onChange?.(this.model.getBlocks());
16711
+ this.host.onSelectionChange?.();
15840
16712
  this.positionTextarea();
15841
16713
  this.target.requestRedraw();
15842
16714
  this.repaint();
@@ -15912,15 +16784,29 @@ function host_signal(signal, abort) {
15912
16784
  //#region src/nodes/text-v2/constants.ts
15913
16785
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15914
16786
  const WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD = 2e5;
16787
+ /** Default list indent per depth, as a multiple of the node `fontSize`. */
16788
+ const WEAVE_TEXT_V2_DEFAULT_INDENT_STEP = 1.5;
15915
16789
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15916
16790
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15917
16791
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15918
- edition: { outline: {
15919
- enabled: true,
15920
- color: "#0074ff",
15921
- width: 1,
15922
- padding: 0
15923
- } },
16792
+ edition: {
16793
+ outline: {
16794
+ enabled: true,
16795
+ color: "#0074ff",
16796
+ width: 1,
16797
+ padding: 0
16798
+ },
16799
+ followPadding: {
16800
+ top: 40,
16801
+ right: 40,
16802
+ bottom: 40,
16803
+ left: 40
16804
+ }
16805
+ },
16806
+ richText: {
16807
+ enabled: false,
16808
+ indentStep: WEAVE_TEXT_V2_DEFAULT_INDENT_STEP
16809
+ },
15924
16810
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15925
16811
  };
15926
16812
  //#endregion
@@ -15948,6 +16834,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15948
16834
  editingNodeId = null;
15949
16835
  session = null;
15950
16836
  sizeWarned = false;
16837
+ selectionTimer;
15951
16838
  nodeRenderedAddedRegistered = false;
15952
16839
  keyPressHandler;
15953
16840
  constructor(params) {
@@ -16021,6 +16908,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16021
16908
  linkPreviousTextDecoration
16022
16909
  }
16023
16910
  });
16911
+ text.setIndentStep(this.indentStep());
16912
+ text.setLinkStyle({
16913
+ color: this.config.link.defaultColor,
16914
+ hoverColor: this.config.link.hoverColor
16915
+ });
16024
16916
  this.setupDefaultNodeAugmentation(text);
16025
16917
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16026
16918
  text.getTransformerProperties = function() {
@@ -16116,7 +17008,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16116
17008
  this.applySize(shape);
16117
17009
  },
16118
17010
  getLayout: () => shape.ensureLayout(),
16119
- measure: (t) => shape.measureText(t),
17011
+ measure: (t, f) => shape.measureText(t, f),
16120
17012
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16121
17013
  getContentOffset: () => shape.getContentOffset(),
16122
17014
  getSize: () => ({
@@ -16167,7 +17059,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16167
17059
  y: p.y * sy
16168
17060
  };
16169
17061
  },
17062
+ revealCaret: (p) => {
17063
+ const box = container;
17064
+ const w = box.clientWidth;
17065
+ const h = box.clientHeight;
17066
+ if (!w || !h) return;
17067
+ const f = this.config.edition.followPadding;
17068
+ const dx = p.x < f.left ? f.left - p.x : p.x > w - f.right ? w - f.right - p.x : 0;
17069
+ const dy = p.y < f.top ? f.top - p.y : p.y > h - f.bottom ? h - f.bottom - p.y : 0;
17070
+ if (!dx && !dy) return;
17071
+ stage.x(stage.x() + dx);
17072
+ stage.y(stage.y() + dy);
17073
+ this.instance.emitEvent("onStageMove");
17074
+ },
16170
17075
  selectAllOnStart: !create,
17076
+ richText: this.isRich(shape),
17077
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17078
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16171
17079
  caretColor: this.config.cursor.color,
16172
17080
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16173
17081
  ...this.config.edition.outline.enabled && {
@@ -16203,6 +17111,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16203
17111
  exitEditMode(shape) {
16204
17112
  const session = this.session;
16205
17113
  if (!session || !this.editing) return;
17114
+ clearTimeout(this.selectionTimer);
16206
17115
  const { changed } = session.getResult();
16207
17116
  session.destroy();
16208
17117
  this.session = null;
@@ -16265,11 +17174,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16265
17174
  text.getLayer()?.batchDraw();
16266
17175
  }
16267
17176
  const layout = text.getAttrs().layout;
16268
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16269
- text.width(text.width() * text.scaleX());
16270
- resetScale(text);
16271
- text.fontSize(text.fontSize() * text.scaleY());
16272
- }
17177
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16273
17178
  text.setAttr("shouldUpdateOnTransform", false);
16274
17179
  text.getLayer()?.batchDraw();
16275
17180
  });
@@ -16313,6 +17218,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16313
17218
  };
16314
17219
  const isLinked = Boolean(nextProps.link);
16315
17220
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17221
+ shape.setIndentStep(this.indentStep());
16316
17222
  shape.setAttrs({
16317
17223
  ...nextProps,
16318
17224
  ...this.outlineProps(),
@@ -16350,7 +17256,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16350
17256
  delete cleanedAttrs.dragBoundFunc;
16351
17257
  if (cleanedAttrs.link) {
16352
17258
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16353
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17259
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16354
17260
  }
16355
17261
  return {
16356
17262
  key: attrs.id ?? "",
@@ -16365,18 +17271,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16365
17271
  }
16366
17272
  };
16367
17273
  }
17274
+ linkCtxMenuGuardRegistered = false;
17275
+ linkHoverText = null;
17276
+ linkModifier = false;
17277
+ linkHoverRefresh = null;
17278
+ linkHitAt(text) {
17279
+ const link = text.getAttrs().link;
17280
+ if (link) {
17281
+ const p = this.localPointer(text);
17282
+ return p.x >= 0 && p.y >= 0 && p.x <= text.width() && p.y <= text.height() ? { url: link } : null;
17283
+ }
17284
+ return text.linkAtPoint(this.localPointer(text));
17285
+ }
17286
+ registerLinkContextMenuGuard() {
17287
+ if (this.linkCtxMenuGuardRegistered || this.instance.isServerSide()) return;
17288
+ this.linkCtxMenuGuardRegistered = true;
17289
+ const stage = this.instance.getStage();
17290
+ const signal = this.instance.getEventsController().signal;
17291
+ const syncModifier = (e) => {
17292
+ const next = e.ctrlKey || e.metaKey;
17293
+ if (next === this.linkModifier) return;
17294
+ this.linkModifier = next;
17295
+ this.linkHoverRefresh?.();
17296
+ };
17297
+ window.addEventListener("keydown", syncModifier, { signal });
17298
+ window.addEventListener("keyup", syncModifier, { signal });
17299
+ window.addEventListener("blur", () => {
17300
+ this.linkModifier = false;
17301
+ this.linkHoverRefresh?.();
17302
+ }, { signal });
17303
+ stage.container().addEventListener("contextmenu", (e) => {
17304
+ if (this.editing || !e.ctrlKey) return;
17305
+ const text = this.linkHoverText;
17306
+ if (!text) return;
17307
+ stage.setPointersPositions(e);
17308
+ const hit = this.linkHitAt(text);
17309
+ if (!hit) return;
17310
+ e.preventDefault();
17311
+ e.stopPropagation();
17312
+ this.openUrl(hit.url);
17313
+ }, {
17314
+ capture: true,
17315
+ signal: this.instance.getEventsController().signal
17316
+ });
17317
+ }
16368
17318
  setupLinkBehavior(text) {
16369
- text.handleMouseover = () => {
16370
- if (!text.getAttrs().link) return;
16371
- text.fill(this.config.link.hoverColor);
16372
- text.getLayer()?.batchDraw();
17319
+ this.registerLinkContextMenuGuard();
17320
+ const container = () => this.instance.getStage().container();
17321
+ const idleCursor = () => this.isNodeSelected(text) ? "grab" : "pointer";
17322
+ const applyHover = () => {
17323
+ if (this.editing) return;
17324
+ this.linkHoverText = text;
17325
+ const hit = this.linkModifier ? this.linkHitAt(text) : null;
17326
+ if (!text.getAttrs().link) text.setHoveredRun(hit?.key ?? null);
17327
+ else {
17328
+ text.fill(hit ? this.config.link.hoverColor : this.config.link.defaultColor);
17329
+ text.getLayer()?.batchDraw();
17330
+ }
17331
+ container().style.cursor = hit ? "pointer" : idleCursor();
17332
+ };
17333
+ const updateHover = (e) => {
17334
+ if (e?.evt) this.linkModifier = e.evt.ctrlKey || e.evt.metaKey;
17335
+ this.linkHoverRefresh = applyHover;
17336
+ applyHover();
17337
+ };
17338
+ text.handleMouseover = (e) => {
17339
+ updateHover(e);
16373
17340
  };
16374
17341
  text.handleMouseout = () => {
17342
+ if (this.linkHoverText === text) {
17343
+ this.linkHoverText = null;
17344
+ this.linkHoverRefresh = null;
17345
+ }
17346
+ text.setHoveredRun(null);
16375
17347
  if (!text.getAttrs().link) return;
16376
17348
  text.fill(this.config.link.defaultColor);
16377
17349
  text.getLayer()?.batchDraw();
16378
17350
  };
16379
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17351
+ text.on("pointermove", updateHover);
17352
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16380
17353
  let linkClickTimeout;
16381
17354
  const clearLinkClickTimeout = () => {
16382
17355
  if (linkClickTimeout) {
@@ -16390,27 +17363,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16390
17363
  previousDblClick();
16391
17364
  };
16392
17365
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16393
- if (!text.getAttrs().link) return;
17366
+ const runLink = text.getAttrs().link ? null : text.linkAtPoint(this.localPointer(text));
17367
+ if (!text.getAttrs().link && !runLink) return;
17368
+ const open = () => runLink ? this.openUrl(runLink.url) : this.openTextLink(text);
16394
17369
  if (ctrlOrMetaPressed) {
16395
17370
  clearLinkClickTimeout();
16396
- this.openTextLink(text);
17371
+ open();
16397
17372
  return;
16398
17373
  }
16399
17374
  if (wasSelected) {
16400
17375
  clearLinkClickTimeout();
16401
17376
  linkClickTimeout = setTimeout(() => {
16402
17377
  linkClickTimeout = void 0;
16403
- this.openTextLink(text);
17378
+ open();
16404
17379
  }, Konva.dblClickWindow);
16405
17380
  }
16406
17381
  };
16407
17382
  }
16408
- openTextLink(textNode) {
17383
+ localPointer(text) {
17384
+ return text.getRelativePointerPosition() ?? {
17385
+ x: -1,
17386
+ y: -1
17387
+ };
17388
+ }
17389
+ lastLinkOpen = 0;
17390
+ openUrl(link) {
16409
17391
  if (this.instance.isServerSide()) return;
16410
- const link = textNode.getAttrs().link;
16411
17392
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17393
+ const now = Date.now();
17394
+ if (now - this.lastLinkOpen < 500) return;
17395
+ this.lastLinkOpen = now;
16412
17396
  window.open(link, "_blank", "noopener,noreferrer");
16413
17397
  }
17398
+ openTextLink(textNode) {
17399
+ this.openUrl(textNode.getAttrs().link);
17400
+ }
16414
17401
  getLink(nodeInstance) {
16415
17402
  const link = nodeInstance.getAttrs().link;
16416
17403
  return typeof link === "string" ? link : void 0;
@@ -16430,6 +17417,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16430
17417
  delete serialized.props.linkPreviousTextDecoration;
16431
17418
  this.instance.updateNode(serialized);
16432
17419
  }
17420
+ indentStep() {
17421
+ const step = this.config.richText.indentStep;
17422
+ return Number.isFinite(step) && step > 0 ? step : 1.5;
17423
+ }
17424
+ /**
17425
+ * Rich text is on for a node when its `richText` attribute says so;
17426
+ * nodes without the attribute follow `config.richText.enabled`.
17427
+ */
17428
+ isRich(nodeInstance) {
17429
+ const own = nodeInstance.getAttrs().richText;
17430
+ return typeof own === "boolean" ? own : this.config.richText.enabled;
17431
+ }
17432
+ isRichTextEnabled(nodeInstance) {
17433
+ return nodeInstance ? this.isRich(nodeInstance) : this.config.richText.enabled;
17434
+ }
17435
+ activeSession(shape) {
17436
+ return this.editing && this.editingNodeId === shape.id() ? this.session : null;
17437
+ }
17438
+ emitFormatChange(shape, reason) {
17439
+ this.instance.emitEvent("onTextFormatChange", {
17440
+ node: shape,
17441
+ format: this.getSelectionFormat(shape),
17442
+ reason
17443
+ });
17444
+ }
17445
+ /** Selection events fire often while dragging: coalesce them. */
17446
+ scheduleSelectionEvent(shape) {
17447
+ if (this.selectionTimer) return;
17448
+ this.selectionTimer = setTimeout(() => {
17449
+ this.selectionTimer = void 0;
17450
+ const session = this.activeSession(shape);
17451
+ if (!session) return;
17452
+ const { start, end } = session.getSelection();
17453
+ this.instance.emitEvent("onTextSelectionChange", {
17454
+ node: shape,
17455
+ start,
17456
+ end,
17457
+ format: session.getFormat()
17458
+ });
17459
+ }, 50);
17460
+ }
17461
+ /**
17462
+ * Format of the active selection while editing, otherwise of `range`
17463
+ * (default: the whole text). Sizes are px.
17464
+ */
17465
+ getSelectionFormat(nodeInstance, range) {
17466
+ const shape = nodeInstance;
17467
+ const session = this.activeSession(shape);
17468
+ if (session) return session.getFormat();
17469
+ const blocks = shape.getBlocks();
17470
+ const r = range ?? {
17471
+ start: 0,
17472
+ end: blocksToText(blocks).length
17473
+ };
17474
+ return getSelectionFormat(blocks, r.start, r.end, shape.fontSize());
17475
+ }
17476
+ /**
17477
+ * Run a block transform on a non-editing node: one `updateNode`, so one
17478
+ * Weave undo step. Returns false when rich text is disabled.
17479
+ */
17480
+ commandOnNode(shape, range, fn) {
17481
+ const blocks = shape.getBlocks();
17482
+ const next = fn(blocks, range?.start ?? 0, range?.end ?? blocksToText(blocks).length);
17483
+ if (JSON.stringify(next) === JSON.stringify(blocks)) return false;
17484
+ shape.setBlocks(next);
17485
+ this.applySize(shape);
17486
+ this.instance.updateNode(this.serialize(shape));
17487
+ this.emitFormatChange(shape, "command");
17488
+ return true;
17489
+ }
17490
+ /**
17491
+ * Set a mark (`undefined` clears it). `size` is in px. While the node is
17492
+ * being edited it applies to the editor selection, otherwise to `range`
17493
+ * (default: the whole text).
17494
+ */
17495
+ applyMark(nodeInstance, key, value, range) {
17496
+ const shape = nodeInstance;
17497
+ if (!this.isRich(shape)) return false;
17498
+ let v = value;
17499
+ if (key === "link" && v !== void 0 && !isValidTextLink(String(v))) return false;
17500
+ if (key === "size" && typeof value === "number") v = sizeToRatio(value, shape.fontSize());
17501
+ const session = this.activeSession(shape);
17502
+ if (session) {
17503
+ session.setMark(key, v);
17504
+ return true;
17505
+ }
17506
+ return this.commandOnNode(shape, range, (b, s, e) => applyMark(b, s, e, key, v));
17507
+ }
17508
+ /** Link a range (or the editor selection) to an http(s) URL. */
17509
+ setRunLink(nodeInstance, url, range) {
17510
+ if (typeof url !== "string" || !isValidTextLink(url)) return false;
17511
+ return this.applyMark(nodeInstance, "link", url, range);
17512
+ }
17513
+ removeRunLink(nodeInstance, range) {
17514
+ return this.applyMark(nodeInstance, "link", void 0, range);
17515
+ }
17516
+ toggleMark(nodeInstance, key, range) {
17517
+ const shape = nodeInstance;
17518
+ if (!this.isRich(shape)) return false;
17519
+ const session = this.activeSession(shape);
17520
+ if (session) {
17521
+ session.toggleMark(key);
17522
+ return true;
17523
+ }
17524
+ return this.commandOnNode(shape, range, (b, s, e) => toggleMark(b, s, e, key));
17525
+ }
17526
+ toggleList(nodeInstance, kind, range) {
17527
+ const shape = nodeInstance;
17528
+ if (!this.isRich(shape)) return false;
17529
+ const session = this.activeSession(shape);
17530
+ if (session) {
17531
+ session.toggleList(kind);
17532
+ return true;
17533
+ }
17534
+ return this.commandOnNode(shape, range, (b, s, e) => toggleList(b, s, e, kind));
17535
+ }
17536
+ /** Align every paragraph touched by the range (default: all). */
17537
+ setAlign(nodeInstance, align, range) {
17538
+ const shape = nodeInstance;
17539
+ if (!this.isRich(shape)) return false;
17540
+ if (align !== void 0 && ![
17541
+ "left",
17542
+ "center",
17543
+ "right"
17544
+ ].includes(align)) return false;
17545
+ const session = this.activeSession(shape);
17546
+ if (session) {
17547
+ session.setAlign(align);
17548
+ return true;
17549
+ }
17550
+ return this.commandOnNode(shape, range, (b, s, e) => setBlockAlign(b, s, e, align));
17551
+ }
17552
+ indent(nodeInstance, range) {
17553
+ const shape = nodeInstance;
17554
+ if (!this.isRich(shape)) return false;
17555
+ const session = this.activeSession(shape);
17556
+ if (session) {
17557
+ session.indent();
17558
+ return true;
17559
+ }
17560
+ return this.commandOnNode(shape, range, indentBlocks);
17561
+ }
17562
+ outdent(nodeInstance, range) {
17563
+ const shape = nodeInstance;
17564
+ if (!this.isRich(shape)) return false;
17565
+ const session = this.activeSession(shape);
17566
+ if (session) {
17567
+ session.outdent();
17568
+ return true;
17569
+ }
17570
+ return this.commandOnNode(shape, range, outdentBlocks);
17571
+ }
17572
+ /**
17573
+ * Scale the whole text: sets the node `fontSize` (px); size marks are
17574
+ * ratios of it, so every run scales with it.
17575
+ */
17576
+ setBaseFontSize(nodeInstance, px) {
17577
+ const shape = nodeInstance;
17578
+ if (!this.isRich(shape)) return false;
17579
+ if (!Number.isFinite(px) || px <= 0) return false;
17580
+ if (shape.fontSize() === px) return false;
17581
+ shape.fontSize(px);
17582
+ this.applySize(shape);
17583
+ if (!this.activeSession(shape)) this.instance.updateNode(this.serialize(shape));
17584
+ this.emitFormatChange(shape, "base-size");
17585
+ return true;
17586
+ }
16433
17587
  resetSmartLayout(textNode) {
16434
17588
  textNode.setAttr("smartFixedWidth", void 0);
16435
17589
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16448,7 +17602,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16448
17602
  return next;
16449
17603
  }
16450
17604
  static getSchema() {
16451
- return WeaveTextNode.getSchema();
17605
+ const base = WeaveTextNode.getSchema();
17606
+ const marks = z.object({
17607
+ bold: z.boolean().optional(),
17608
+ italic: z.boolean().optional(),
17609
+ underline: z.boolean().optional(),
17610
+ strike: z.boolean().optional(),
17611
+ color: z.string().optional(),
17612
+ size: z.number().positive().optional().describe("Ratio of the node fontSize (1 is omitted)."),
17613
+ family: z.string().optional(),
17614
+ lineHeight: z.number().positive().optional(),
17615
+ link: z.string().url().refine((value) => isValidTextLink(value), { message: "Link must be an absolute http:// or https:// URL." }).optional()
17616
+ }).passthrough();
17617
+ const block = z.object({
17618
+ runs: z.array(z.object({
17619
+ t: z.string(),
17620
+ m: marks.optional()
17621
+ })),
17622
+ kind: z.enum(["ul", "ol"]).optional(),
17623
+ depth: z.number().int().min(0).optional(),
17624
+ start: z.number().int().min(1).optional(),
17625
+ align: z.enum([
17626
+ "left",
17627
+ "center",
17628
+ "right"
17629
+ ]).optional()
17630
+ });
17631
+ return base.extend({ props: base.shape.props.extend({
17632
+ richText: z.boolean().optional().describe("Enables rich text commands for this node."),
17633
+ blocks: z.array(block).optional().describe("Rich content: paragraphs of runs with marks and optional list fields. `text` is the derived plain mirror.")
17634
+ }) });
16452
17635
  }
16453
17636
  };
16454
17637
  //#endregion
@@ -26970,8 +28153,16 @@ var WeaveTextToolAction = class extends WeaveAction {
26970
28153
  cancelAction;
26971
28154
  onPropsChange = void 0;
26972
28155
  onInit = void 0;
26973
- constructor() {
28156
+ toolName;
28157
+ initialProps;
28158
+ /**
28159
+ * `name` and `props` let an app register several text tools, e.g. a
28160
+ * plain one and one creating rich text nodes (`props: { richText: true }`).
28161
+ */
28162
+ constructor(params) {
26974
28163
  super();
28164
+ this.toolName = params?.name ?? "textTool";
28165
+ this.initialProps = params?.props ?? {};
26975
28166
  this.initialize();
26976
28167
  }
26977
28168
  initialize() {
@@ -26983,7 +28174,7 @@ var WeaveTextToolAction = class extends WeaveAction {
26983
28174
  this.props = this.initProps();
26984
28175
  }
26985
28176
  getName() {
26986
- return TEXT_TOOL_ACTION_NAME;
28177
+ return this.toolName;
26987
28178
  }
26988
28179
  initProps() {
26989
28180
  return {
@@ -26995,7 +28186,8 @@ var WeaveTextToolAction = class extends WeaveAction {
26995
28186
  align: "left",
26996
28187
  lineHeight: 1,
26997
28188
  verticalAlign: "top",
26998
- strokeEnabled: false
28189
+ strokeEnabled: false,
28190
+ ...this.initialProps
26999
28191
  };
27000
28192
  }
27001
28193
  setupEvents() {