@inditextech/weave-sdk 5.7.0-SNAPSHOT.597.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.597.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;
@@ -15055,8 +15650,9 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15055
15650
  const det = m.a * m.d - m.b * m.c;
15056
15651
  if (!det || !Number.isFinite(det)) return null;
15057
15652
  const canvas = ctx.getCanvas();
15058
- const w = canvas.getWidth();
15059
- const h = canvas.getHeight();
15653
+ const backing = canvas._canvas;
15654
+ const w = backing?.width ?? canvas.getWidth() * canvas.getPixelRatio();
15655
+ const h = backing?.height ?? canvas.getHeight() * canvas.getPixelRatio();
15060
15656
  if (!(w > 0) || !(h > 0)) return null;
15061
15657
  let y1 = Infinity;
15062
15658
  let y2 = -Infinity;
@@ -15080,30 +15676,22 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15080
15676
  const layout = self.ensureLayout();
15081
15677
  if (layout.lines.length === 0) return;
15082
15678
  const padding = self.padding();
15083
- const lineHeightPx = layout.lineHeightPx;
15084
15679
  let baseline = "middle";
15085
- let translateY = lineHeightPx / 2;
15086
- if (!Konva.legacyTextRendering) {
15087
- const m = self.measureMetrics();
15088
- baseline = "alphabetic";
15089
- translateY = (m.ascent - m.descent) / 2 + lineHeightPx / 2;
15090
- }
15680
+ if (!Konva.legacyTextRendering) baseline = "alphabetic";
15091
15681
  ctx.setAttr("font", fontString(self.style()));
15092
15682
  ctx.setAttr("textBaseline", baseline);
15093
15683
  ctx.setAttr("textAlign", "left");
15094
15684
  const alignY = self.verticalOffset(layout);
15095
15685
  const range = self.visibleRange(ctx);
15096
15686
  ctx.translate(padding, alignY + padding);
15097
- const top = alignY + padding;
15098
- for (let n = 0; n < layout.lines.length; n++) {
15099
- const lineTop = top + n * lineHeightPx;
15100
- if (range && (lineTop + lineHeightPx < range.y1 - lineHeightPx || lineTop > range.y2 + lineHeightPx)) continue;
15101
- 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);
15102
15690
  }
15103
15691
  };
15104
15692
  /** Vertical offset of the text block inside the box (verticalAlign). */
15105
15693
  verticalOffset(layout) {
15106
- const used = layout.lines.length * layout.lineHeightPx + this.padding() * 2;
15694
+ const used = layout.contentHeight + this.padding() * 2;
15107
15695
  const va = this.verticalAlign();
15108
15696
  if (va === "middle") return (this.getHeight() - used) / 2;
15109
15697
  if (va === "bottom") return this.getHeight() - used;
@@ -15114,7 +15702,21 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15114
15702
  const fontSize = this.fontSize();
15115
15703
  ctx.save();
15116
15704
  if (decoration.includes("underline")) this.strokeDecoration(ctx, line, y + Math.round(fontSize / 4));
15117
- 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
+ }
15118
15720
  if (decoration.includes("line-through")) this.strokeDecoration(ctx, line, y - Math.round(fontSize / 4));
15119
15721
  ctx.restore();
15120
15722
  }
@@ -15128,24 +15730,44 @@ var WeaveTextV2Shape = class extends Konva.Shape {
15128
15730
  ctx.stroke();
15129
15731
  ctx.restore();
15130
15732
  }
15131
- 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) {
15132
15744
  const spacing = this.letterSpacing();
15133
15745
  if (spacing === 0) {
15134
- this.fillText(ctx, text, x, y);
15746
+ this.fillText(ctx, seg.t, x, y, color);
15135
15747
  return;
15136
15748
  }
15137
15749
  let cx = x;
15138
- for (const ch of Array.from(text)) {
15139
- this.fillText(ctx, ch, cx, y);
15140
- 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;
15141
15753
  }
15142
15754
  }
15143
- fillText(ctx, text, x, y) {
15755
+ fillText(ctx, text, x, y, color) {
15144
15756
  const self = this;
15145
15757
  self._partialText = text;
15146
15758
  self._partialTextX = x;
15147
15759
  self._partialTextY = y;
15148
- 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
+ }
15149
15771
  }
15150
15772
  _hitFunc = (ctx, shape) => {
15151
15773
  ctx.beginPath();
@@ -15261,9 +15883,27 @@ function lineIndexAt(layout, offset) {
15261
15883
  }
15262
15884
  return fallback;
15263
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
+ }
15264
15904
  function lineX(line, offset, measure) {
15265
15905
  const k = Math.max(0, Math.min(offset - line.start, line.text.length));
15266
- return line.offsetX + (k === 0 ? 0 : measure(line.text.slice(0, k)));
15906
+ return line.offsetX + prefixWidth(line, k, measure);
15267
15907
  }
15268
15908
  /** Caret rectangle in text-local coordinates (before padding/valign offset). */
15269
15909
  function caretRect(layout, offset, measure) {
@@ -15277,16 +15917,22 @@ function caretRect(layout, offset, measure) {
15277
15917
  };
15278
15918
  return {
15279
15919
  x: lineX(line, offset, measure),
15280
- y: i * layout.lineHeightPx,
15281
- height: layout.lineHeightPx,
15920
+ y: line.y,
15921
+ height: line.height,
15282
15922
  line: i
15283
15923
  };
15284
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
+ }
15285
15931
  /** Offset closest to a point in text-local coordinates. */
15286
15932
  function offsetFromPoint(layout, point, measure) {
15287
15933
  const lines = layout.lines;
15288
15934
  if (lines.length === 0) return 0;
15289
- 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);
15290
15936
  }
15291
15937
  function offsetInLine(line, x, measure) {
15292
15938
  const rel = x - line.offsetX;
@@ -15294,7 +15940,7 @@ function offsetInLine(line, x, measure) {
15294
15940
  const b = boundaries(line.text, graphemeSeg);
15295
15941
  let prevW = 0;
15296
15942
  for (let k = 1; k < b.length; k++) {
15297
- const w = measure(line.text.slice(0, b[k]));
15943
+ const w = prefixWidth(line, b[k], measure);
15298
15944
  if (rel < (prevW + w) / 2) return line.start + b[k - 1];
15299
15945
  prevW = w;
15300
15946
  }
@@ -15318,7 +15964,7 @@ function selectionRects(layout, from, to, measure) {
15318
15964
  if (from > to) [from, to] = [to, from];
15319
15965
  if (from === to) return [];
15320
15966
  const out = [];
15321
- layout.lines.forEach((line, i) => {
15967
+ layout.lines.forEach((line) => {
15322
15968
  const hasNewline = line.lastInParagraph;
15323
15969
  const lineFrom = Math.max(from, line.start);
15324
15970
  const lineTo = Math.min(to, hasNewline ? line.end + 1 : line.end);
@@ -15329,9 +15975,9 @@ function selectionRects(layout, from, to, measure) {
15329
15975
  if (x2 <= x1) return;
15330
15976
  out.push({
15331
15977
  x: x1,
15332
- y: i * layout.lineHeightPx,
15978
+ y: line.y,
15333
15979
  width: x2 - x1,
15334
- height: layout.lineHeightPx
15980
+ height: line.height
15335
15981
  });
15336
15982
  });
15337
15983
  return out;
@@ -15350,6 +15996,11 @@ var TextEditModel = class {
15350
15996
  focus = 0;
15351
15997
  /** x kept while moving vertically; reset by any other move. */
15352
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;
15353
16004
  undoStack = [];
15354
16005
  redoStack = [];
15355
16006
  lastKind = null;
@@ -15386,12 +16037,14 @@ var TextEditModel = class {
15386
16037
  const len = this.getText().length;
15387
16038
  this.anchor = Math.max(0, Math.min(anchor, len));
15388
16039
  this.focus = Math.max(0, Math.min(focus, len));
16040
+ this.storedMarks = null;
15389
16041
  this.breakCoalescing();
15390
16042
  }
15391
16043
  moveTo(offset, extend = false, keepDesiredX = false) {
15392
16044
  const len = this.getText().length;
15393
16045
  const o = Math.max(0, Math.min(offset, len));
15394
16046
  this.focus = o;
16047
+ this.storedMarks = null;
15395
16048
  if (!extend) this.anchor = o;
15396
16049
  if (!keepDesiredX) this.desiredX = null;
15397
16050
  this.breakCoalescing();
@@ -15399,6 +16052,7 @@ var TextEditModel = class {
15399
16052
  selectAll() {
15400
16053
  this.anchor = 0;
15401
16054
  this.focus = this.getText().length;
16055
+ this.storedMarks = null;
15402
16056
  this.breakCoalescing();
15403
16057
  }
15404
16058
  breakCoalescing() {
@@ -15434,7 +16088,14 @@ var TextEditModel = class {
15434
16088
  end = Math.max(start, Math.min(end, len));
15435
16089
  if (start === end && text === "") return false;
15436
16090
  this.record(kind, start);
15437
- 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
+ }
15438
16099
  this.anchor = this.focus = start + text.length;
15439
16100
  this.lastEditEnd = this.focus;
15440
16101
  this.desiredX = null;
@@ -15474,14 +16135,102 @@ var TextEditModel = class {
15474
16135
  this.restore(next);
15475
16136
  return true;
15476
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
+ }
15477
16218
  restore(s) {
15478
16219
  this.blocks = s.blocks;
16220
+ this.storedMarks = null;
15479
16221
  this.anchor = s.anchor;
15480
16222
  this.focus = s.focus;
15481
16223
  this.lastKind = null;
15482
16224
  this.desiredX = null;
15483
16225
  }
15484
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
+ }
15485
16234
  const SLICE_BUDGET_MS = 8;
15486
16235
  const BLINK_MS = 530;
15487
16236
  var TextEditSession = class {
@@ -15539,7 +16288,7 @@ var TextEditSession = class {
15539
16288
  };
15540
16289
  if (this.host.selectAllOnStart) this.model.selectAll();
15541
16290
  this.syncTextarea();
15542
- ta.focus();
16291
+ ta.focus({ preventScroll: true });
15543
16292
  this.positionTextarea();
15544
16293
  this.startBlink();
15545
16294
  this.repaint();
@@ -15568,7 +16317,7 @@ var TextEditSession = class {
15568
16317
  if (w.weaveTextEditing) delete w.weaveTextEditing[this.id];
15569
16318
  }
15570
16319
  focus() {
15571
- this.textarea?.focus();
16320
+ this.textarea?.focus({ preventScroll: true });
15572
16321
  }
15573
16322
  createTextarea() {
15574
16323
  const ta = this.host.container.ownerDocument.createElement("textarea");
@@ -15617,8 +16366,12 @@ var TextEditSession = class {
15617
16366
  if (!ta) return;
15618
16367
  const c = this.caretStagePoint();
15619
16368
  const p = this.host.stageToContainer(c);
15620
- ta.style.left = `${p.x}px`;
15621
- 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`;
15622
16375
  }
15623
16376
  caretStagePoint() {
15624
16377
  const r = caretRect(this.target.getLayout(), this.model.focus, this.target.measure);
@@ -15650,10 +16403,41 @@ var TextEditSession = class {
15650
16403
  if (e.key === "Escape") this.host.onRequestExit("escape");
15651
16404
  else if (isUndo) this.undo();
15652
16405
  else if (isRedo) this.redo();
16406
+ else if (this.handleRichKey(e, mod, key)) return true;
15653
16407
  else if (e.key === "Tab") return true;
15654
16408
  else return false;
15655
16409
  return true;
15656
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
+ }
15657
16441
  handleNavigationKey(e) {
15658
16442
  if (e.ctrlKey || e.metaKey) return false;
15659
16443
  if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey) {
@@ -15707,7 +16491,15 @@ var TextEditSession = class {
15707
16491
  while (p < max && oldText[p] === newText[p]) p++;
15708
16492
  let s = 0;
15709
16493
  while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
15710
- 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
+ }
15711
16503
  const inputType = e.inputType ?? "";
15712
16504
  const kind = this.editKindFor(inputType);
15713
16505
  this.model.replaceRange(p, oldText.length - s, inserted, kind);
@@ -15748,6 +16540,7 @@ var TextEditSession = class {
15748
16540
  if (this.model.undo()) {
15749
16541
  this.syncTextarea();
15750
16542
  this.afterContentChange(0, true);
16543
+ this.host.onFormatChange?.("undo");
15751
16544
  }
15752
16545
  }
15753
16546
  redo() {
@@ -15755,8 +16548,42 @@ var TextEditSession = class {
15755
16548
  if (this.model.redo()) {
15756
16549
  this.syncTextarea();
15757
16550
  this.afterContentChange(0, true);
16551
+ this.host.onFormatChange?.("redo");
15758
16552
  }
15759
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
+ }
15760
16587
  toLocal(e) {
15761
16588
  const stagePt = this.host.clientToStage(e.clientX, e.clientY);
15762
16589
  const p = this.target.getAbsoluteTransform().copy().invert().point(stagePt);
@@ -15771,12 +16598,26 @@ var TextEditSession = class {
15771
16598
  const { width, height } = this.target.getSize();
15772
16599
  return local.x + o.x >= 0 && local.y + o.y >= 0 && local.x + o.x <= width && local.y + o.y <= height;
15773
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
+ }
15774
16610
  onMouseDown = (e) => {
16611
+ if (this.isKeepTarget(e)) {
16612
+ if (!e.target.closest("input, textarea, select, [contenteditable=\"true\"]")) e.preventDefault();
16613
+ return;
16614
+ }
15775
16615
  if (this.isInside(this.toLocal(e))) e.preventDefault();
15776
16616
  };
15777
16617
  onPointerDown = (e) => {
15778
16618
  if (e.button !== 0 && e.pointerType === "mouse") return;
15779
16619
  if (this.textarea && e.target === this.textarea) return;
16620
+ if (this.isKeepTarget(e)) return;
15780
16621
  const local = this.toLocal(e);
15781
16622
  if (!this.isInside(local)) {
15782
16623
  e.stopPropagation();
@@ -15787,10 +16628,11 @@ var TextEditSession = class {
15787
16628
  e.preventDefault();
15788
16629
  const offset = offsetFromPoint(this.target.getLayout(), local, this.target.measure);
15789
16630
  const text = this.model.getText();
15790
- if (e.detail >= 3) {
16631
+ const clicks = this.countClicks(e);
16632
+ if (clicks >= 3) {
15791
16633
  const [s, en] = paragraphRangeAt(text, offset);
15792
16634
  this.model.setSelection(s, en);
15793
- } else if (e.detail === 2) {
16635
+ } else if (clicks === 2) {
15794
16636
  const [s, en] = wordRangeAt(text, offset);
15795
16637
  this.model.setSelection(s, en);
15796
16638
  } else {
@@ -15799,8 +16641,31 @@ var TextEditSession = class {
15799
16641
  }
15800
16642
  this.syncTextarea();
15801
16643
  this.afterSelectionChange();
15802
- this.textarea?.focus();
16644
+ this.textarea?.focus({ preventScroll: true });
15803
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
+ }
15804
16669
  onPointerMove = (e) => {
15805
16670
  if (!this.dragging) return;
15806
16671
  const local = this.toLocal(e);
@@ -15812,13 +16677,14 @@ var TextEditSession = class {
15812
16677
  onPointerUp = () => {
15813
16678
  if (!this.dragging) return;
15814
16679
  this.dragging = false;
15815
- this.textarea?.focus();
16680
+ this.textarea?.focus({ preventScroll: true });
15816
16681
  };
15817
16682
  afterSelectionChange() {
15818
16683
  this.syncTextarea();
15819
16684
  this.positionTextarea();
15820
16685
  this.resetBlink();
15821
16686
  this.repaint();
16687
+ this.host.onSelectionChange?.();
15822
16688
  }
15823
16689
  afterContentChange(insertedLength, immediate = false) {
15824
16690
  this.flushPending();
@@ -15842,6 +16708,7 @@ var TextEditSession = class {
15842
16708
  applyContent() {
15843
16709
  this.target.setBlocks(this.model.getBlocks());
15844
16710
  this.host.onChange?.(this.model.getBlocks());
16711
+ this.host.onSelectionChange?.();
15845
16712
  this.positionTextarea();
15846
16713
  this.target.requestRedraw();
15847
16714
  this.repaint();
@@ -15917,15 +16784,29 @@ function host_signal(signal, abort) {
15917
16784
  //#region src/nodes/text-v2/constants.ts
15918
16785
  /** Default threshold for `onTextSizeWarning` (chars of serialized blocks). */
15919
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;
15920
16789
  const WEAVE_TEXT_V2_SELECTION_COLOR = "rgba(66, 133, 244, 0.35)";
15921
16790
  const WEAVE_TEXT_V2_DEFAULT_CONFIG = {
15922
16791
  ...WEAVE_TEXT_NODE_DEFAULT_CONFIG,
15923
- edition: { outline: {
15924
- enabled: true,
15925
- color: "#0074ff",
15926
- width: 1,
15927
- padding: 0
15928
- } },
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
+ },
15929
16810
  sizeWarning: { threshold: WEAVE_TEXT_V2_SIZE_WARNING_THRESHOLD }
15930
16811
  };
15931
16812
  //#endregion
@@ -15953,6 +16834,7 @@ var WeaveTextV2Node = class extends WeaveNode {
15953
16834
  editingNodeId = null;
15954
16835
  session = null;
15955
16836
  sizeWarned = false;
16837
+ selectionTimer;
15956
16838
  nodeRenderedAddedRegistered = false;
15957
16839
  keyPressHandler;
15958
16840
  constructor(params) {
@@ -16026,6 +16908,11 @@ var WeaveTextV2Node = class extends WeaveNode {
16026
16908
  linkPreviousTextDecoration
16027
16909
  }
16028
16910
  });
16911
+ text.setIndentStep(this.indentStep());
16912
+ text.setLinkStyle({
16913
+ color: this.config.link.defaultColor,
16914
+ hoverColor: this.config.link.hoverColor
16915
+ });
16029
16916
  this.setupDefaultNodeAugmentation(text);
16030
16917
  const defaultTransformerProperties = this.defaultGetTransformerProperties(this.config.transform);
16031
16918
  text.getTransformerProperties = function() {
@@ -16121,7 +17008,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16121
17008
  this.applySize(shape);
16122
17009
  },
16123
17010
  getLayout: () => shape.ensureLayout(),
16124
- measure: (t) => shape.measureText(t),
17011
+ measure: (t, f) => shape.measureText(t, f),
16125
17012
  getAbsoluteTransform: () => shape.getAbsoluteTransform(),
16126
17013
  getContentOffset: () => shape.getContentOffset(),
16127
17014
  getSize: () => ({
@@ -16172,7 +17059,23 @@ var WeaveTextV2Node = class extends WeaveNode {
16172
17059
  y: p.y * sy
16173
17060
  };
16174
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
+ },
16175
17075
  selectAllOnStart: !create,
17076
+ richText: this.isRich(shape),
17077
+ onSelectionChange: () => this.scheduleSelectionEvent(shape),
17078
+ onFormatChange: (reason) => this.emitFormatChange(shape, reason),
16176
17079
  caretColor: this.config.cursor.color,
16177
17080
  selectionColor: WEAVE_TEXT_V2_SELECTION_COLOR,
16178
17081
  ...this.config.edition.outline.enabled && {
@@ -16208,6 +17111,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16208
17111
  exitEditMode(shape) {
16209
17112
  const session = this.session;
16210
17113
  if (!session || !this.editing) return;
17114
+ clearTimeout(this.selectionTimer);
16211
17115
  const { changed } = session.getResult();
16212
17116
  session.destroy();
16213
17117
  this.session = null;
@@ -16270,11 +17174,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16270
17174
  text.getLayer()?.batchDraw();
16271
17175
  }
16272
17176
  const layout = text.getAttrs().layout;
16273
- if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) {
16274
- text.width(text.width() * text.scaleX());
16275
- resetScale(text);
16276
- text.fontSize(text.fontSize() * text.scaleY());
16277
- }
17177
+ if (this.isSelecting() && this.isNodeSelected(text) && (layout !== TEXT_LAYOUT.SMART || !isCorner())) resetScale(text);
16278
17178
  text.setAttr("shouldUpdateOnTransform", false);
16279
17179
  text.getLayer()?.batchDraw();
16280
17180
  });
@@ -16318,6 +17218,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16318
17218
  };
16319
17219
  const isLinked = Boolean(nextProps.link);
16320
17220
  const { linkPreviousFill, linkPreviousTextDecoration } = this.linkOverrides(nextProps);
17221
+ shape.setIndentStep(this.indentStep());
16321
17222
  shape.setAttrs({
16322
17223
  ...nextProps,
16323
17224
  ...this.outlineProps(),
@@ -16355,7 +17256,7 @@ var WeaveTextV2Node = class extends WeaveNode {
16355
17256
  delete cleanedAttrs.dragBoundFunc;
16356
17257
  if (cleanedAttrs.link) {
16357
17258
  if (typeof cleanedAttrs.linkPreviousFill === "string") cleanedAttrs.fill = cleanedAttrs.linkPreviousFill;
16358
- if (typeof cleanedAttrs.linkPreviousTextDecoration === "string") cleanedAttrs.textDecoration = cleanedAttrs.linkPreviousTextDecoration;
17259
+ cleanedAttrs.textDecoration = typeof cleanedAttrs.linkPreviousTextDecoration === "string" ? cleanedAttrs.linkPreviousTextDecoration : "";
16359
17260
  }
16360
17261
  return {
16361
17262
  key: attrs.id ?? "",
@@ -16370,18 +17271,85 @@ var WeaveTextV2Node = class extends WeaveNode {
16370
17271
  }
16371
17272
  };
16372
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
+ }
16373
17318
  setupLinkBehavior(text) {
16374
- text.handleMouseover = () => {
16375
- if (!text.getAttrs().link) return;
16376
- text.fill(this.config.link.hoverColor);
16377
- 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);
16378
17340
  };
16379
17341
  text.handleMouseout = () => {
17342
+ if (this.linkHoverText === text) {
17343
+ this.linkHoverText = null;
17344
+ this.linkHoverRefresh = null;
17345
+ }
17346
+ text.setHoveredRun(null);
16380
17347
  if (!text.getAttrs().link) return;
16381
17348
  text.fill(this.config.link.defaultColor);
16382
17349
  text.getLayer()?.batchDraw();
16383
17350
  };
16384
- text.defineMousePointer = () => text.getAttrs().link ? "pointer" : "default";
17351
+ text.on("pointermove", updateHover);
17352
+ text.defineMousePointer = () => this.linkModifier && text.getAttrs().link ? "pointer" : idleCursor();
16385
17353
  let linkClickTimeout;
16386
17354
  const clearLinkClickTimeout = () => {
16387
17355
  if (linkClickTimeout) {
@@ -16395,27 +17363,41 @@ var WeaveTextV2Node = class extends WeaveNode {
16395
17363
  previousDblClick();
16396
17364
  };
16397
17365
  text.click = ({ wasSelected, ctrlOrMetaPressed }) => {
16398
- 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);
16399
17369
  if (ctrlOrMetaPressed) {
16400
17370
  clearLinkClickTimeout();
16401
- this.openTextLink(text);
17371
+ open();
16402
17372
  return;
16403
17373
  }
16404
17374
  if (wasSelected) {
16405
17375
  clearLinkClickTimeout();
16406
17376
  linkClickTimeout = setTimeout(() => {
16407
17377
  linkClickTimeout = void 0;
16408
- this.openTextLink(text);
17378
+ open();
16409
17379
  }, Konva.dblClickWindow);
16410
17380
  }
16411
17381
  };
16412
17382
  }
16413
- openTextLink(textNode) {
17383
+ localPointer(text) {
17384
+ return text.getRelativePointerPosition() ?? {
17385
+ x: -1,
17386
+ y: -1
17387
+ };
17388
+ }
17389
+ lastLinkOpen = 0;
17390
+ openUrl(link) {
16414
17391
  if (this.instance.isServerSide()) return;
16415
- const link = textNode.getAttrs().link;
16416
17392
  if (typeof link !== "string" || !isValidTextLink(link)) return;
17393
+ const now = Date.now();
17394
+ if (now - this.lastLinkOpen < 500) return;
17395
+ this.lastLinkOpen = now;
16417
17396
  window.open(link, "_blank", "noopener,noreferrer");
16418
17397
  }
17398
+ openTextLink(textNode) {
17399
+ this.openUrl(textNode.getAttrs().link);
17400
+ }
16419
17401
  getLink(nodeInstance) {
16420
17402
  const link = nodeInstance.getAttrs().link;
16421
17403
  return typeof link === "string" ? link : void 0;
@@ -16435,6 +17417,173 @@ var WeaveTextV2Node = class extends WeaveNode {
16435
17417
  delete serialized.props.linkPreviousTextDecoration;
16436
17418
  this.instance.updateNode(serialized);
16437
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
+ }
16438
17587
  resetSmartLayout(textNode) {
16439
17588
  textNode.setAttr("smartFixedWidth", void 0);
16440
17589
  textNode.width(Math.max(1, textNode.measureNaturalSize().width * 1.01));
@@ -16453,7 +17602,36 @@ var WeaveTextV2Node = class extends WeaveNode {
16453
17602
  return next;
16454
17603
  }
16455
17604
  static getSchema() {
16456
- 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
+ }) });
16457
17635
  }
16458
17636
  };
16459
17637
  //#endregion
@@ -26975,8 +28153,16 @@ var WeaveTextToolAction = class extends WeaveAction {
26975
28153
  cancelAction;
26976
28154
  onPropsChange = void 0;
26977
28155
  onInit = void 0;
26978
- 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) {
26979
28163
  super();
28164
+ this.toolName = params?.name ?? "textTool";
28165
+ this.initialProps = params?.props ?? {};
26980
28166
  this.initialize();
26981
28167
  }
26982
28168
  initialize() {
@@ -26988,7 +28174,7 @@ var WeaveTextToolAction = class extends WeaveAction {
26988
28174
  this.props = this.initProps();
26989
28175
  }
26990
28176
  getName() {
26991
- return TEXT_TOOL_ACTION_NAME;
28177
+ return this.toolName;
26992
28178
  }
26993
28179
  initProps() {
26994
28180
  return {
@@ -27000,7 +28186,8 @@ var WeaveTextToolAction = class extends WeaveAction {
27000
28186
  align: "left",
27001
28187
  lineHeight: 1,
27002
28188
  verticalAlign: "top",
27003
- strokeEnabled: false
28189
+ strokeEnabled: false,
28190
+ ...this.initialProps
27004
28191
  };
27005
28192
  }
27006
28193
  setupEvents() {