@harbour-enterprises/superdoc 1.6.0-next.1 → 1.6.0-next.2

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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const jszip = require("./jszip-C8_CqJxM.cjs");
3
3
  const helpers$1 = require("./helpers-nOdwpmwb.cjs");
4
- const superEditor_converter = require("./SuperConverter-DpKjVrPl.cjs");
4
+ const superEditor_converter = require("./SuperConverter-DrhNM5Cd.cjs");
5
5
  const vue = require("./vue-De9wkgLl.cjs");
6
6
  require("./jszip.min-BPh2MMAa.cjs");
7
7
  const eventemitter3 = require("./eventemitter3-BQuRcMPI.cjs");
@@ -9387,6 +9387,158 @@ class Schema {
9387
9387
  return Object.fromEntries(markEntries);
9388
9388
  }
9389
9389
  }
9390
+ const positionTrackerKey = new superEditor_converter.PluginKey("positionTracker");
9391
+ function createPositionTrackerPlugin() {
9392
+ return new superEditor_converter.Plugin({
9393
+ key: positionTrackerKey,
9394
+ state: {
9395
+ init() {
9396
+ return {
9397
+ decorations: DecorationSet.empty,
9398
+ generation: 0
9399
+ };
9400
+ },
9401
+ apply(tr, state) {
9402
+ let { decorations, generation } = state;
9403
+ const meta = tr.getMeta(positionTrackerKey);
9404
+ if (meta?.action === "add") {
9405
+ decorations = decorations.add(tr.doc, meta.decorations);
9406
+ } else if (meta?.action === "remove") {
9407
+ const toRemove = decorations.find().filter((decoration) => meta.ids.includes(decoration.spec.id));
9408
+ decorations = decorations.remove(toRemove);
9409
+ } else if (meta?.action === "removeByType") {
9410
+ const toRemove = decorations.find().filter((decoration) => decoration.spec.type === meta.type);
9411
+ decorations = decorations.remove(toRemove);
9412
+ }
9413
+ if (tr.docChanged) {
9414
+ decorations = decorations.map(tr.mapping, tr.doc);
9415
+ generation += 1;
9416
+ }
9417
+ return { decorations, generation };
9418
+ }
9419
+ },
9420
+ props: {
9421
+ decorations() {
9422
+ return DecorationSet.empty;
9423
+ }
9424
+ }
9425
+ });
9426
+ }
9427
+ class PositionTracker {
9428
+ #editor;
9429
+ constructor(editor) {
9430
+ this.#editor = editor;
9431
+ }
9432
+ #getState() {
9433
+ if (!this.#editor?.state) return null;
9434
+ return positionTrackerKey.getState(this.#editor.state) ?? null;
9435
+ }
9436
+ track(from3, to, spec) {
9437
+ const id = uuid.v4();
9438
+ if (!this.#editor?.state) return id;
9439
+ const fullSpec = { kind: "range", ...spec, id };
9440
+ const deco = Decoration.inline(from3, to, {}, fullSpec);
9441
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9442
+ action: "add",
9443
+ decorations: [deco]
9444
+ }).setMeta("addToHistory", false);
9445
+ this.#editor.dispatch(tr);
9446
+ return id;
9447
+ }
9448
+ trackMany(ranges) {
9449
+ if (!this.#editor?.state) {
9450
+ return ranges.map(() => uuid.v4());
9451
+ }
9452
+ const ids = [];
9453
+ const decorations = [];
9454
+ for (const { from: from3, to, spec } of ranges) {
9455
+ const id = uuid.v4();
9456
+ ids.push(id);
9457
+ const fullSpec = { kind: "range", ...spec, id };
9458
+ decorations.push(Decoration.inline(from3, to, {}, fullSpec));
9459
+ }
9460
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9461
+ action: "add",
9462
+ decorations
9463
+ }).setMeta("addToHistory", false);
9464
+ this.#editor.dispatch(tr);
9465
+ return ids;
9466
+ }
9467
+ untrack(id) {
9468
+ if (!this.#editor?.state) return;
9469
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9470
+ action: "remove",
9471
+ ids: [id]
9472
+ }).setMeta("addToHistory", false);
9473
+ this.#editor.dispatch(tr);
9474
+ }
9475
+ untrackMany(ids) {
9476
+ if (!this.#editor?.state || ids.length === 0) return;
9477
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9478
+ action: "remove",
9479
+ ids
9480
+ }).setMeta("addToHistory", false);
9481
+ this.#editor.dispatch(tr);
9482
+ }
9483
+ untrackByType(type) {
9484
+ if (!this.#editor?.state) return;
9485
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9486
+ action: "removeByType",
9487
+ type
9488
+ }).setMeta("addToHistory", false);
9489
+ this.#editor.dispatch(tr);
9490
+ }
9491
+ resolve(id) {
9492
+ const state = this.#getState();
9493
+ if (!state) return null;
9494
+ const found = state.decorations.find().find((decoration) => decoration.spec.id === id);
9495
+ if (!found) return null;
9496
+ const spec = found.spec;
9497
+ return {
9498
+ id: spec.id,
9499
+ from: found.from,
9500
+ to: found.to,
9501
+ spec
9502
+ };
9503
+ }
9504
+ resolveMany(ids) {
9505
+ const result = /* @__PURE__ */ new Map();
9506
+ for (const id of ids) {
9507
+ result.set(id, null);
9508
+ }
9509
+ const state = this.#getState();
9510
+ if (!state || ids.length === 0) return result;
9511
+ const idSet = new Set(ids);
9512
+ for (const decoration of state.decorations.find()) {
9513
+ const spec = decoration.spec;
9514
+ if (idSet.has(spec.id)) {
9515
+ result.set(spec.id, {
9516
+ id: spec.id,
9517
+ from: decoration.from,
9518
+ to: decoration.to,
9519
+ spec
9520
+ });
9521
+ }
9522
+ }
9523
+ return result;
9524
+ }
9525
+ findByType(type) {
9526
+ const state = this.#getState();
9527
+ if (!state) return [];
9528
+ return state.decorations.find().filter((decoration) => decoration.spec.type === type).map((decoration) => {
9529
+ const spec = decoration.spec;
9530
+ return {
9531
+ id: spec.id,
9532
+ from: decoration.from,
9533
+ to: decoration.to,
9534
+ spec
9535
+ };
9536
+ });
9537
+ }
9538
+ get generation() {
9539
+ return this.#getState()?.generation ?? 0;
9540
+ }
9541
+ }
9390
9542
  class OxmlNode extends Node$1 {
9391
9543
  constructor(config) {
9392
9544
  super(config);
@@ -11747,6 +11899,34 @@ const EditorFocus = Extension.create({
11747
11899
  return [editorFocusPlugin];
11748
11900
  }
11749
11901
  });
11902
+ const PositionTrackerExtension = Extension.create({
11903
+ name: "positionTracker",
11904
+ addStorage() {
11905
+ return {
11906
+ tracker: null
11907
+ };
11908
+ },
11909
+ addPmPlugins() {
11910
+ return [createPositionTrackerPlugin()];
11911
+ },
11912
+ onCreate() {
11913
+ const existing = this.editor?.positionTracker ?? this.storage.tracker;
11914
+ if (existing) {
11915
+ this.storage.tracker = existing;
11916
+ this.editor.positionTracker = existing;
11917
+ return;
11918
+ }
11919
+ const tracker = new PositionTracker(this.editor);
11920
+ this.storage.tracker = tracker;
11921
+ this.editor.positionTracker = tracker;
11922
+ },
11923
+ onDestroy() {
11924
+ if (this.editor?.positionTracker === this.storage.tracker) {
11925
+ this.editor.positionTracker = null;
11926
+ }
11927
+ this.storage.tracker = null;
11928
+ }
11929
+ });
11750
11930
  class EventEmitter {
11751
11931
  #events = /* @__PURE__ */ new Map();
11752
11932
  /**
@@ -15524,7 +15704,7 @@ const canUseDOM = () => {
15524
15704
  return false;
15525
15705
  }
15526
15706
  };
15527
- const summaryVersion = "1.6.0-next.1";
15707
+ const summaryVersion = "1.6.0-next.2";
15528
15708
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
15529
15709
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
15530
15710
  function mapAttributes(attrs) {
@@ -17017,7 +17197,7 @@ class Editor extends EventEmitter {
17017
17197
  */
17018
17198
  #createExtensionService() {
17019
17199
  const allowedExtensions = ["extension", "node", "mark"];
17020
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
17200
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
17021
17201
  const externalExtensions = this.options.externalExtensions || [];
17022
17202
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
17023
17203
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -18191,7 +18371,7 @@ class Editor extends EventEmitter {
18191
18371
  * Process collaboration migrations
18192
18372
  */
18193
18373
  processCollaborationMigrations() {
18194
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
18374
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.2");
18195
18375
  if (!this.options.ydoc) return;
18196
18376
  const metaMap = this.options.ydoc.getMap("meta");
18197
18377
  let docVersion = metaMap.get("version");
@@ -75544,14 +75724,317 @@ function getMatchHighlights(state) {
75544
75724
  let search2 = searchKey.getState(state);
75545
75725
  return search2 ? search2.deco : DecorationSet.empty;
75546
75726
  }
75547
- function setSearchState(tr, query, range = null, options = {}) {
75548
- if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75549
- throw new TypeError("setSearchState options must be an object");
75727
+ const BLOCK_SEPARATOR = "\n";
75728
+ const ATOM_PLACEHOLDER = "";
75729
+ class SearchIndex {
75730
+ /** @type {string} */
75731
+ text = "";
75732
+ /** @type {Segment[]} */
75733
+ segments = [];
75734
+ /** @type {boolean} */
75735
+ valid = false;
75736
+ /** @type {number} */
75737
+ docSize = 0;
75738
+ /**
75739
+ * Build the search index from a ProseMirror document.
75740
+ * Uses doc.textBetween for the flattened string and walks
75741
+ * the document to build the segment offset map.
75742
+ *
75743
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
75744
+ */
75745
+ build(doc2) {
75746
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
75747
+ this.segments = [];
75748
+ this.docSize = doc2.content.size;
75749
+ let offset2 = 0;
75750
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
75751
+ this.segments.push(segment);
75752
+ offset2 = segment.offsetEnd;
75753
+ });
75754
+ this.valid = true;
75755
+ }
75756
+ /**
75757
+ * Walk the content of a node to build segments.
75758
+ * This method processes the children of a node, given the position
75759
+ * where the node's content starts.
75760
+ *
75761
+ * @param {import('prosemirror-model').Node} node - Current node
75762
+ * @param {number} contentStart - Document position where this node's content starts
75763
+ * @param {number} offset - Current offset in flattened string
75764
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75765
+ * @returns {number} The new offset after processing this node's content
75766
+ */
75767
+ #walkNodeContent(node, contentStart, offset2, addSegment) {
75768
+ let currentOffset = offset2;
75769
+ let isFirstChild = true;
75770
+ node.forEach((child, childContentOffset) => {
75771
+ const childDocPos = contentStart + childContentOffset;
75772
+ if (child.isBlock && !isFirstChild) {
75773
+ addSegment({
75774
+ offsetStart: currentOffset,
75775
+ offsetEnd: currentOffset + 1,
75776
+ docFrom: childDocPos,
75777
+ docTo: childDocPos,
75778
+ kind: "blockSep"
75779
+ });
75780
+ currentOffset += 1;
75781
+ }
75782
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
75783
+ isFirstChild = false;
75784
+ });
75785
+ return currentOffset;
75786
+ }
75787
+ /**
75788
+ * Recursively walk a node and its descendants to build segments.
75789
+ *
75790
+ * @param {import('prosemirror-model').Node} node - Current node
75791
+ * @param {number} docPos - Document position at start of this node
75792
+ * @param {number} offset - Current offset in flattened string
75793
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75794
+ * @returns {number} The new offset after processing this node
75795
+ */
75796
+ #walkNode(node, docPos, offset2, addSegment) {
75797
+ if (node.isText) {
75798
+ const text = node.text || "";
75799
+ if (text.length > 0) {
75800
+ addSegment({
75801
+ offsetStart: offset2,
75802
+ offsetEnd: offset2 + text.length,
75803
+ docFrom: docPos,
75804
+ docTo: docPos + text.length,
75805
+ kind: "text"
75806
+ });
75807
+ return offset2 + text.length;
75808
+ }
75809
+ return offset2;
75810
+ }
75811
+ if (node.isLeaf) {
75812
+ if (node.type.name === "hard_break") {
75813
+ addSegment({
75814
+ offsetStart: offset2,
75815
+ offsetEnd: offset2 + 1,
75816
+ docFrom: docPos,
75817
+ docTo: docPos + node.nodeSize,
75818
+ kind: "hardBreak"
75819
+ });
75820
+ return offset2 + 1;
75821
+ }
75822
+ addSegment({
75823
+ offsetStart: offset2,
75824
+ offsetEnd: offset2 + 1,
75825
+ docFrom: docPos,
75826
+ docTo: docPos + node.nodeSize,
75827
+ kind: "atom"
75828
+ });
75829
+ return offset2 + 1;
75830
+ }
75831
+ return this.#walkNodeContent(node, docPos + 1, offset2, addSegment);
75832
+ }
75833
+ /**
75834
+ * Mark the index as stale. It will be rebuilt on next search.
75835
+ */
75836
+ invalidate() {
75837
+ this.valid = false;
75838
+ }
75839
+ /**
75840
+ * Check if the index needs rebuilding for the given document.
75841
+ *
75842
+ * @param {import('prosemirror-model').Node} doc - The document to check against
75843
+ * @returns {boolean} True if index is stale and needs rebuilding
75844
+ */
75845
+ isStale(doc2) {
75846
+ return !this.valid || doc2.content.size !== this.docSize;
75847
+ }
75848
+ /**
75849
+ * Ensure the index is valid for the given document.
75850
+ * Rebuilds if stale.
75851
+ *
75852
+ * @param {import('prosemirror-model').Node} doc - The document
75853
+ */
75854
+ ensureValid(doc2) {
75855
+ if (this.isStale(doc2)) {
75856
+ this.build(doc2);
75857
+ }
75858
+ }
75859
+ /**
75860
+ * Convert an offset range in the flattened string to document ranges.
75861
+ * Skips separator/atom segments and returns only text ranges.
75862
+ *
75863
+ * @param {number} start - Start offset in flattened string
75864
+ * @param {number} end - End offset in flattened string
75865
+ * @returns {DocRange[]} Array of document ranges (text segments only)
75866
+ */
75867
+ offsetRangeToDocRanges(start2, end2) {
75868
+ const ranges = [];
75869
+ for (const segment of this.segments) {
75870
+ if (segment.offsetEnd <= start2) continue;
75871
+ if (segment.offsetStart >= end2) break;
75872
+ if (segment.kind !== "text") continue;
75873
+ const overlapStart = Math.max(start2, segment.offsetStart);
75874
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
75875
+ if (overlapStart < overlapEnd) {
75876
+ const startInSegment = overlapStart - segment.offsetStart;
75877
+ const endInSegment = overlapEnd - segment.offsetStart;
75878
+ ranges.push({
75879
+ from: segment.docFrom + startInSegment,
75880
+ to: segment.docFrom + endInSegment
75881
+ });
75882
+ }
75883
+ }
75884
+ return ranges;
75885
+ }
75886
+ /**
75887
+ * Find the document position for a given offset in the flattened string.
75888
+ *
75889
+ * @param {number} offset - Offset in flattened string
75890
+ * @returns {number|null} Document position, or null if not found
75891
+ */
75892
+ offsetToDocPos(offset2) {
75893
+ for (const segment of this.segments) {
75894
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
75895
+ if (segment.kind === "text") {
75896
+ return segment.docFrom + (offset2 - segment.offsetStart);
75897
+ }
75898
+ return segment.docFrom;
75899
+ }
75900
+ }
75901
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
75902
+ const lastSeg = this.segments[this.segments.length - 1];
75903
+ return lastSeg.docTo;
75904
+ }
75905
+ return null;
75906
+ }
75907
+ /**
75908
+ * Escape special regex characters in a string.
75909
+ *
75910
+ * @param {string} str - String to escape
75911
+ * @returns {string} Escaped string safe for use in RegExp
75912
+ */
75913
+ static escapeRegex(str) {
75914
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75915
+ }
75916
+ /**
75917
+ * Convert a plain search string to a whitespace-flexible regex pattern.
75918
+ * This allows matching across paragraph boundaries.
75919
+ *
75920
+ * @param {string} searchString - The search string
75921
+ * @returns {string} Regex pattern string
75922
+ */
75923
+ static toFlexiblePattern(searchString) {
75924
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
75925
+ if (parts.length === 0) return "";
75926
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
75927
+ }
75928
+ /**
75929
+ * Search the index for matches.
75930
+ *
75931
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
75932
+ * @param {Object} options - Search options
75933
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
75934
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
75935
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
75936
+ */
75937
+ search(pattern, options = {}) {
75938
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
75939
+ const matches = [];
75940
+ let regex;
75941
+ if (pattern instanceof RegExp) {
75942
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
75943
+ regex = new RegExp(pattern.source, flags);
75944
+ } else if (typeof pattern === "string") {
75945
+ if (pattern.length === 0) return matches;
75946
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
75947
+ if (flexiblePattern.length === 0) return matches;
75948
+ const flags = caseSensitive ? "g" : "gi";
75949
+ regex = new RegExp(flexiblePattern, flags);
75950
+ } else {
75951
+ return matches;
75952
+ }
75953
+ let match;
75954
+ while ((match = regex.exec(this.text)) !== null && matches.length < maxMatches) {
75955
+ matches.push({
75956
+ start: match.index,
75957
+ end: match.index + match[0].length,
75958
+ text: match[0]
75959
+ });
75960
+ if (match[0].length === 0) {
75961
+ regex.lastIndex++;
75962
+ }
75963
+ }
75964
+ return matches;
75550
75965
  }
75551
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75552
- return tr.setMeta(searchKey, { query, range, highlight });
75553
75966
  }
75967
+ const customSearchHighlightsKey = new superEditor_converter.PluginKey("customSearchHighlights");
75554
75968
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
75969
+ const resolveInlineTextPosition = (doc2, position, direction) => {
75970
+ const docSize = doc2.content.size;
75971
+ if (!Number.isFinite(position) || position < 0 || position > docSize) {
75972
+ return position;
75973
+ }
75974
+ const step = direction === "forward" ? 1 : -1;
75975
+ let current = position;
75976
+ let iterations = 0;
75977
+ while (iterations < 8) {
75978
+ iterations += 1;
75979
+ const resolved = doc2.resolve(current);
75980
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
75981
+ if (!boundaryNode) break;
75982
+ if (boundaryNode.isText) break;
75983
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
75984
+ const next = current + step;
75985
+ if (next < 0 || next > docSize) break;
75986
+ current = next;
75987
+ const adjacent = doc2.resolve(current);
75988
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
75989
+ if (checkNode && checkNode.isText) break;
75990
+ }
75991
+ return current;
75992
+ };
75993
+ const resolveSearchRange = ({ doc: doc2, from: from3, to, expectedText, highlights }) => {
75994
+ const docSize = doc2.content.size;
75995
+ let resolvedFrom = Math.max(0, Math.min(from3, docSize));
75996
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
75997
+ if (highlights) {
75998
+ const windowStart = Math.max(0, resolvedFrom - 4);
75999
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
76000
+ const candidates = highlights.find(windowStart, windowEnd);
76001
+ if (candidates.length > 0) {
76002
+ let chosen = candidates[0];
76003
+ if (expectedText) {
76004
+ const matching = candidates.filter(
76005
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
76006
+ );
76007
+ if (matching.length > 0) {
76008
+ chosen = matching[0];
76009
+ }
76010
+ }
76011
+ resolvedFrom = chosen.from;
76012
+ resolvedTo = chosen.to;
76013
+ }
76014
+ }
76015
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
76016
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
76017
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
76018
+ resolvedFrom = normalizedFrom;
76019
+ resolvedTo = normalizedTo;
76020
+ }
76021
+ return { from: resolvedFrom, to: resolvedTo };
76022
+ };
76023
+ const getPositionTracker = (editor) => {
76024
+ if (!editor) return null;
76025
+ if (editor.positionTracker) return editor.positionTracker;
76026
+ const storageTracker = editor.storage?.positionTracker?.tracker;
76027
+ if (storageTracker) {
76028
+ editor.positionTracker = storageTracker;
76029
+ return storageTracker;
76030
+ }
76031
+ const tracker = new PositionTracker(editor);
76032
+ if (editor.storage?.positionTracker) {
76033
+ editor.storage.positionTracker.tracker = tracker;
76034
+ }
76035
+ editor.positionTracker = tracker;
76036
+ return tracker;
76037
+ };
75555
76038
  const Search = Extension.create({
75556
76039
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
75557
76040
  addStorage() {
@@ -75560,29 +76043,58 @@ const Search = Extension.create({
75560
76043
  * @private
75561
76044
  * @type {SearchMatch[]|null}
75562
76045
  */
75563
- searchResults: []
76046
+ searchResults: [],
76047
+ /**
76048
+ * @private
76049
+ * @type {boolean}
76050
+ * Whether to apply CSS highlight classes to matches
76051
+ */
76052
+ highlightEnabled: true,
76053
+ /**
76054
+ * @private
76055
+ * @type {SearchIndex}
76056
+ * Lazily-built search index for cross-paragraph matching
76057
+ */
76058
+ searchIndex: new SearchIndex()
75564
76059
  };
75565
76060
  },
75566
76061
  addPmPlugins() {
75567
76062
  const editor = this.editor;
75568
76063
  const storage = this.storage;
76064
+ const searchIndexInvalidatorPlugin = new superEditor_converter.Plugin({
76065
+ key: new superEditor_converter.PluginKey("searchIndexInvalidator"),
76066
+ appendTransaction(transactions, oldState, newState) {
76067
+ const docChanged = transactions.some((tr) => tr.docChanged);
76068
+ if (docChanged && storage?.searchIndex) {
76069
+ storage.searchIndex.invalidate();
76070
+ }
76071
+ return null;
76072
+ }
76073
+ });
75569
76074
  const searchHighlightWithIdPlugin = new superEditor_converter.Plugin({
75570
- key: new superEditor_converter.PluginKey("customSearchHighlights"),
76075
+ key: customSearchHighlightsKey,
75571
76076
  props: {
75572
76077
  decorations(state) {
75573
76078
  if (!editor) return null;
75574
76079
  const matches = storage?.searchResults;
75575
76080
  if (!matches?.length) return null;
75576
- const decorations = matches.map(
75577
- (match) => Decoration.inline(match.from, match.to, {
75578
- id: `search-match-${match.id}`
75579
- })
75580
- );
76081
+ const highlightEnabled = storage?.highlightEnabled !== false;
76082
+ const decorations = [];
76083
+ for (const match of matches) {
76084
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
76085
+ if (match.ranges && match.ranges.length > 0) {
76086
+ for (const range of match.ranges) {
76087
+ decorations.push(Decoration.inline(range.from, range.to, attrs));
76088
+ }
76089
+ } else {
76090
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
76091
+ }
76092
+ }
75581
76093
  return DecorationSet.create(state.doc, decorations);
75582
76094
  }
75583
76095
  }
75584
76096
  });
75585
- return [search(), searchHighlightWithIdPlugin];
76097
+ return [search(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
75586
76098
  },
75587
76099
  addCommands() {
75588
76100
  return {
@@ -75596,21 +76108,51 @@ const Search = Extension.create({
75596
76108
  goToFirstMatch: () => (
75597
76109
  /** @returns {boolean} */
75598
76110
  ({ state, editor, dispatch }) => {
76111
+ const searchResults = this.storage?.searchResults;
76112
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
76113
+ const firstMatch = searchResults[0];
76114
+ const from3 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
76115
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
76116
+ if (typeof from3 !== "number" || typeof to !== "number") {
76117
+ return false;
76118
+ }
76119
+ editor.view.focus();
76120
+ const tr2 = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, from3, to)).scrollIntoView();
76121
+ if (dispatch) dispatch(tr2);
76122
+ const presentationEditor2 = editor.presentationEditor;
76123
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
76124
+ const didScroll = presentationEditor2.scrollToPosition(from3, { block: "center" });
76125
+ if (didScroll) return true;
76126
+ }
76127
+ try {
76128
+ const domPos = editor.view.domAtPos(from3);
76129
+ if (domPos?.node?.scrollIntoView) {
76130
+ domPos.node.scrollIntoView(true);
76131
+ }
76132
+ } catch {
76133
+ }
76134
+ return true;
76135
+ }
75599
76136
  const highlights = getMatchHighlights(state);
75600
76137
  if (!highlights) return false;
75601
76138
  const decorations = highlights.find();
75602
76139
  if (!decorations?.length) return false;
75603
- const firstMatch = decorations[0];
76140
+ const firstDeco = decorations[0];
75604
76141
  editor.view.focus();
75605
- const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
76142
+ const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
75606
76143
  if (dispatch) dispatch(tr);
75607
76144
  const presentationEditor = editor.presentationEditor;
75608
76145
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
75609
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
76146
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
75610
76147
  if (didScroll) return true;
75611
76148
  }
75612
- const domPos = editor.view.domAtPos(firstMatch.from);
75613
- domPos?.node?.scrollIntoView(true);
76149
+ try {
76150
+ const domPos = editor.view.domAtPos(firstDeco.from);
76151
+ if (domPos?.node?.scrollIntoView) {
76152
+ domPos.node.scrollIntoView(true);
76153
+ }
76154
+ } catch {
76155
+ }
75614
76156
  return true;
75615
76157
  }
75616
76158
  ),
@@ -75628,53 +76170,57 @@ const Search = Extension.create({
75628
76170
  *
75629
76171
  * // Search without visual highlighting
75630
76172
  * const silentMatches = editor.commands.search('test', { highlight: false })
75631
- * @note Returns array of SearchMatch objects with positions and IDs
76173
+ *
76174
+ * // Cross-paragraph search (works by default for plain strings)
76175
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
76176
+ * @note Returns array of SearchMatch objects with positions and IDs.
76177
+ * Plain string searches are whitespace-flexible and match across paragraphs.
76178
+ * Regex searches match exactly as specified.
75632
76179
  */
75633
76180
  search: (patternInput, options = {}) => (
75634
76181
  /** @returns {SearchMatch[]} */
75635
- ({ state, dispatch }) => {
76182
+ ({ state, dispatch, editor }) => {
75636
76183
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75637
76184
  throw new TypeError("Search options must be an object");
75638
76185
  }
75639
76186
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75640
- let pattern;
76187
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
75641
76188
  let caseSensitive = false;
75642
- let regexp = false;
75643
- const wholeWord = false;
76189
+ let searchPattern = patternInput;
75644
76190
  if (isRegExp(patternInput)) {
75645
- const regexPattern = (
75646
- /** @type {RegExp} */
75647
- patternInput
75648
- );
75649
- regexp = true;
75650
- pattern = regexPattern.source;
75651
- caseSensitive = !regexPattern.flags.includes("i");
76191
+ caseSensitive = !patternInput.flags.includes("i");
76192
+ searchPattern = patternInput;
75652
76193
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
75653
76194
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
75654
- regexp = true;
75655
- pattern = body;
75656
76195
  caseSensitive = !flags.includes("i");
76196
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
75657
76197
  } else {
75658
- pattern = String(patternInput);
76198
+ searchPattern = String(patternInput);
75659
76199
  }
75660
- const query = new SearchQuery({
75661
- search: pattern,
76200
+ const searchIndex = this.storage.searchIndex;
76201
+ searchIndex.ensureValid(state.doc);
76202
+ const indexMatches = searchIndex.search(searchPattern, {
75662
76203
  caseSensitive,
75663
- regexp,
75664
- wholeWord
76204
+ maxMatches
75665
76205
  });
75666
- const tr = setSearchState(state.tr, query, null, { highlight });
75667
- dispatch(tr);
75668
- const newState = state.apply(tr);
75669
- const decoSet = getMatchHighlights(newState);
75670
- const matches = decoSet ? decoSet.find() : [];
75671
- const resultMatches = matches.map((d) => ({
75672
- from: d.from,
75673
- to: d.to,
75674
- text: newState.doc.textBetween(d.from, d.to),
75675
- id: uuid.v4()
75676
- }));
76206
+ const resultMatches = [];
76207
+ for (const indexMatch of indexMatches) {
76208
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
76209
+ if (ranges.length === 0) continue;
76210
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
76211
+ const combinedText = matchTexts.join("");
76212
+ const match = {
76213
+ from: ranges[0].from,
76214
+ to: ranges[ranges.length - 1].to,
76215
+ text: combinedText,
76216
+ id: uuid.v4(),
76217
+ ranges,
76218
+ trackerIds: []
76219
+ };
76220
+ resultMatches.push(match);
76221
+ }
75677
76222
  this.storage.searchResults = resultMatches;
76223
+ this.storage.highlightEnabled = highlight;
75678
76224
  return resultMatches;
75679
76225
  }
75680
76226
  ),
@@ -75685,12 +76231,48 @@ const Search = Extension.create({
75685
76231
  * @example
75686
76232
  * const searchResults = editor.commands.search('test string')
75687
76233
  * editor.commands.goToSearchResult(searchResults[3])
75688
- * @note Scrolls to match and selects it
76234
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
76235
+ * selects the first range and scrolls to it.
75689
76236
  */
75690
76237
  goToSearchResult: (match) => (
75691
76238
  /** @returns {boolean} */
75692
76239
  ({ state, dispatch, editor }) => {
75693
- const { from: from3, to } = match;
76240
+ const positionTracker = getPositionTracker(editor);
76241
+ const doc2 = state.doc;
76242
+ const highlights = getMatchHighlights(state);
76243
+ let from3, to;
76244
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
76245
+ if (positionTracker?.resolve && match.trackerIds[0]) {
76246
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
76247
+ if (resolved) {
76248
+ from3 = resolved.from;
76249
+ to = resolved.to;
76250
+ }
76251
+ }
76252
+ if (from3 === void 0) {
76253
+ from3 = match.ranges[0].from;
76254
+ to = match.ranges[0].to;
76255
+ }
76256
+ } else {
76257
+ from3 = match.from;
76258
+ to = match.to;
76259
+ if (positionTracker?.resolve && match?.id) {
76260
+ const resolved = positionTracker.resolve(match.id);
76261
+ if (resolved) {
76262
+ from3 = resolved.from;
76263
+ to = resolved.to;
76264
+ }
76265
+ }
76266
+ }
76267
+ const normalized = resolveSearchRange({
76268
+ doc: doc2,
76269
+ from: from3,
76270
+ to,
76271
+ expectedText: match?.text ?? null,
76272
+ highlights
76273
+ });
76274
+ from3 = normalized.from;
76275
+ to = normalized.to;
75694
76276
  editor.view.focus();
75695
76277
  const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, from3, to)).scrollIntoView();
75696
76278
  if (dispatch) dispatch(tr);