@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.
@@ -38097,7 +38097,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
38097
38097
  static getStoredSuperdocVersion(docx) {
38098
38098
  return SuperConverter.getStoredCustomProperty(docx, "SuperdocVersion");
38099
38099
  }
38100
- static setStoredSuperdocVersion(docx = this.convertedXml, version2 = "1.6.0-next.1") {
38100
+ static setStoredSuperdocVersion(docx = this.convertedXml, version2 = "1.6.0-next.2") {
38101
38101
  return SuperConverter.setStoredCustomProperty(docx, "SuperdocVersion", version2, false);
38102
38102
  }
38103
38103
  /**
@@ -47391,6 +47391,158 @@ Please report this to https://github.com/markedjs/marked.`, e) {
47391
47391
  return Object.fromEntries(markEntries);
47392
47392
  }
47393
47393
  };
47394
+ const positionTrackerKey = new PluginKey("positionTracker");
47395
+ function createPositionTrackerPlugin() {
47396
+ return new Plugin({
47397
+ key: positionTrackerKey,
47398
+ state: {
47399
+ init() {
47400
+ return {
47401
+ decorations: DecorationSet.empty,
47402
+ generation: 0
47403
+ };
47404
+ },
47405
+ apply(tr, state) {
47406
+ let { decorations, generation } = state;
47407
+ const meta2 = tr.getMeta(positionTrackerKey);
47408
+ if (meta2?.action === "add") {
47409
+ decorations = decorations.add(tr.doc, meta2.decorations);
47410
+ } else if (meta2?.action === "remove") {
47411
+ const toRemove = decorations.find().filter((decoration) => meta2.ids.includes(decoration.spec.id));
47412
+ decorations = decorations.remove(toRemove);
47413
+ } else if (meta2?.action === "removeByType") {
47414
+ const toRemove = decorations.find().filter((decoration) => decoration.spec.type === meta2.type);
47415
+ decorations = decorations.remove(toRemove);
47416
+ }
47417
+ if (tr.docChanged) {
47418
+ decorations = decorations.map(tr.mapping, tr.doc);
47419
+ generation += 1;
47420
+ }
47421
+ return { decorations, generation };
47422
+ }
47423
+ },
47424
+ props: {
47425
+ decorations() {
47426
+ return DecorationSet.empty;
47427
+ }
47428
+ }
47429
+ });
47430
+ }
47431
+ class PositionTracker {
47432
+ #editor;
47433
+ constructor(editor) {
47434
+ this.#editor = editor;
47435
+ }
47436
+ #getState() {
47437
+ if (!this.#editor?.state) return null;
47438
+ return positionTrackerKey.getState(this.#editor.state) ?? null;
47439
+ }
47440
+ track(from2, to, spec) {
47441
+ const id = v4();
47442
+ if (!this.#editor?.state) return id;
47443
+ const fullSpec = { kind: "range", ...spec, id };
47444
+ const deco = Decoration.inline(from2, to, {}, fullSpec);
47445
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
47446
+ action: "add",
47447
+ decorations: [deco]
47448
+ }).setMeta("addToHistory", false);
47449
+ this.#editor.dispatch(tr);
47450
+ return id;
47451
+ }
47452
+ trackMany(ranges) {
47453
+ if (!this.#editor?.state) {
47454
+ return ranges.map(() => v4());
47455
+ }
47456
+ const ids = [];
47457
+ const decorations = [];
47458
+ for (const { from: from2, to, spec } of ranges) {
47459
+ const id = v4();
47460
+ ids.push(id);
47461
+ const fullSpec = { kind: "range", ...spec, id };
47462
+ decorations.push(Decoration.inline(from2, to, {}, fullSpec));
47463
+ }
47464
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
47465
+ action: "add",
47466
+ decorations
47467
+ }).setMeta("addToHistory", false);
47468
+ this.#editor.dispatch(tr);
47469
+ return ids;
47470
+ }
47471
+ untrack(id) {
47472
+ if (!this.#editor?.state) return;
47473
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
47474
+ action: "remove",
47475
+ ids: [id]
47476
+ }).setMeta("addToHistory", false);
47477
+ this.#editor.dispatch(tr);
47478
+ }
47479
+ untrackMany(ids) {
47480
+ if (!this.#editor?.state || ids.length === 0) return;
47481
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
47482
+ action: "remove",
47483
+ ids
47484
+ }).setMeta("addToHistory", false);
47485
+ this.#editor.dispatch(tr);
47486
+ }
47487
+ untrackByType(type) {
47488
+ if (!this.#editor?.state) return;
47489
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
47490
+ action: "removeByType",
47491
+ type
47492
+ }).setMeta("addToHistory", false);
47493
+ this.#editor.dispatch(tr);
47494
+ }
47495
+ resolve(id) {
47496
+ const state = this.#getState();
47497
+ if (!state) return null;
47498
+ const found2 = state.decorations.find().find((decoration) => decoration.spec.id === id);
47499
+ if (!found2) return null;
47500
+ const spec = found2.spec;
47501
+ return {
47502
+ id: spec.id,
47503
+ from: found2.from,
47504
+ to: found2.to,
47505
+ spec
47506
+ };
47507
+ }
47508
+ resolveMany(ids) {
47509
+ const result = /* @__PURE__ */ new Map();
47510
+ for (const id of ids) {
47511
+ result.set(id, null);
47512
+ }
47513
+ const state = this.#getState();
47514
+ if (!state || ids.length === 0) return result;
47515
+ const idSet = new Set(ids);
47516
+ for (const decoration of state.decorations.find()) {
47517
+ const spec = decoration.spec;
47518
+ if (idSet.has(spec.id)) {
47519
+ result.set(spec.id, {
47520
+ id: spec.id,
47521
+ from: decoration.from,
47522
+ to: decoration.to,
47523
+ spec
47524
+ });
47525
+ }
47526
+ }
47527
+ return result;
47528
+ }
47529
+ findByType(type) {
47530
+ const state = this.#getState();
47531
+ if (!state) return [];
47532
+ return state.decorations.find().filter((decoration) => decoration.spec.type === type).map((decoration) => {
47533
+ const spec = decoration.spec;
47534
+ return {
47535
+ id: spec.id,
47536
+ from: decoration.from,
47537
+ to: decoration.to,
47538
+ spec
47539
+ };
47540
+ });
47541
+ }
47542
+ get generation() {
47543
+ return this.#getState()?.generation ?? 0;
47544
+ }
47545
+ }
47394
47546
  class OxmlNode extends Node$2 {
47395
47547
  constructor(config2) {
47396
47548
  super(config2);
@@ -49751,6 +49903,34 @@ Please report this to https://github.com/markedjs/marked.`, e) {
49751
49903
  return [editorFocusPlugin];
49752
49904
  }
49753
49905
  });
49906
+ const PositionTrackerExtension = Extension.create({
49907
+ name: "positionTracker",
49908
+ addStorage() {
49909
+ return {
49910
+ tracker: null
49911
+ };
49912
+ },
49913
+ addPmPlugins() {
49914
+ return [createPositionTrackerPlugin()];
49915
+ },
49916
+ onCreate() {
49917
+ const existing = this.editor?.positionTracker ?? this.storage.tracker;
49918
+ if (existing) {
49919
+ this.storage.tracker = existing;
49920
+ this.editor.positionTracker = existing;
49921
+ return;
49922
+ }
49923
+ const tracker = new PositionTracker(this.editor);
49924
+ this.storage.tracker = tracker;
49925
+ this.editor.positionTracker = tracker;
49926
+ },
49927
+ onDestroy() {
49928
+ if (this.editor?.positionTracker === this.storage.tracker) {
49929
+ this.editor.positionTracker = null;
49930
+ }
49931
+ this.storage.tracker = null;
49932
+ }
49933
+ });
49754
49934
  let EventEmitter$1 = class EventEmitter {
49755
49935
  #events = /* @__PURE__ */ new Map();
49756
49936
  /**
@@ -64003,7 +64183,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
64003
64183
  return false;
64004
64184
  }
64005
64185
  };
64006
- const summaryVersion = "1.6.0-next.1";
64186
+ const summaryVersion = "1.6.0-next.2";
64007
64187
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
64008
64188
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
64009
64189
  function mapAttributes(attrs) {
@@ -65497,7 +65677,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
65497
65677
  */
65498
65678
  #createExtensionService() {
65499
65679
  const allowedExtensions = ["extension", "node", "mark"];
65500
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
65680
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
65501
65681
  const externalExtensions = this.options.externalExtensions || [];
65502
65682
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
65503
65683
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -66671,7 +66851,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
66671
66851
  * Process collaboration migrations
66672
66852
  */
66673
66853
  processCollaborationMigrations() {
66674
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
66854
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.2");
66675
66855
  if (!this.options.ydoc) return;
66676
66856
  const metaMap = this.options.ydoc.getMap("meta");
66677
66857
  let docVersion = metaMap.get("version");
@@ -123801,14 +123981,317 @@ ${o}
123801
123981
  let search2 = searchKey.getState(state);
123802
123982
  return search2 ? search2.deco : DecorationSet.empty;
123803
123983
  }
123804
- function setSearchState(tr, query, range2 = null, options = {}) {
123805
- if (options != null && (typeof options !== "object" || Array.isArray(options))) {
123806
- throw new TypeError("setSearchState options must be an object");
123984
+ const BLOCK_SEPARATOR = "\n";
123985
+ const ATOM_PLACEHOLDER = "";
123986
+ class SearchIndex {
123987
+ /** @type {string} */
123988
+ text = "";
123989
+ /** @type {Segment[]} */
123990
+ segments = [];
123991
+ /** @type {boolean} */
123992
+ valid = false;
123993
+ /** @type {number} */
123994
+ docSize = 0;
123995
+ /**
123996
+ * Build the search index from a ProseMirror document.
123997
+ * Uses doc.textBetween for the flattened string and walks
123998
+ * the document to build the segment offset map.
123999
+ *
124000
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
124001
+ */
124002
+ build(doc2) {
124003
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
124004
+ this.segments = [];
124005
+ this.docSize = doc2.content.size;
124006
+ let offset2 = 0;
124007
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
124008
+ this.segments.push(segment);
124009
+ offset2 = segment.offsetEnd;
124010
+ });
124011
+ this.valid = true;
124012
+ }
124013
+ /**
124014
+ * Walk the content of a node to build segments.
124015
+ * This method processes the children of a node, given the position
124016
+ * where the node's content starts.
124017
+ *
124018
+ * @param {import('prosemirror-model').Node} node - Current node
124019
+ * @param {number} contentStart - Document position where this node's content starts
124020
+ * @param {number} offset - Current offset in flattened string
124021
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
124022
+ * @returns {number} The new offset after processing this node's content
124023
+ */
124024
+ #walkNodeContent(node2, contentStart, offset2, addSegment) {
124025
+ let currentOffset = offset2;
124026
+ let isFirstChild = true;
124027
+ node2.forEach((child, childContentOffset) => {
124028
+ const childDocPos = contentStart + childContentOffset;
124029
+ if (child.isBlock && !isFirstChild) {
124030
+ addSegment({
124031
+ offsetStart: currentOffset,
124032
+ offsetEnd: currentOffset + 1,
124033
+ docFrom: childDocPos,
124034
+ docTo: childDocPos,
124035
+ kind: "blockSep"
124036
+ });
124037
+ currentOffset += 1;
124038
+ }
124039
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
124040
+ isFirstChild = false;
124041
+ });
124042
+ return currentOffset;
124043
+ }
124044
+ /**
124045
+ * Recursively walk a node and its descendants to build segments.
124046
+ *
124047
+ * @param {import('prosemirror-model').Node} node - Current node
124048
+ * @param {number} docPos - Document position at start of this node
124049
+ * @param {number} offset - Current offset in flattened string
124050
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
124051
+ * @returns {number} The new offset after processing this node
124052
+ */
124053
+ #walkNode(node2, docPos, offset2, addSegment) {
124054
+ if (node2.isText) {
124055
+ const text2 = node2.text || "";
124056
+ if (text2.length > 0) {
124057
+ addSegment({
124058
+ offsetStart: offset2,
124059
+ offsetEnd: offset2 + text2.length,
124060
+ docFrom: docPos,
124061
+ docTo: docPos + text2.length,
124062
+ kind: "text"
124063
+ });
124064
+ return offset2 + text2.length;
124065
+ }
124066
+ return offset2;
124067
+ }
124068
+ if (node2.isLeaf) {
124069
+ if (node2.type.name === "hard_break") {
124070
+ addSegment({
124071
+ offsetStart: offset2,
124072
+ offsetEnd: offset2 + 1,
124073
+ docFrom: docPos,
124074
+ docTo: docPos + node2.nodeSize,
124075
+ kind: "hardBreak"
124076
+ });
124077
+ return offset2 + 1;
124078
+ }
124079
+ addSegment({
124080
+ offsetStart: offset2,
124081
+ offsetEnd: offset2 + 1,
124082
+ docFrom: docPos,
124083
+ docTo: docPos + node2.nodeSize,
124084
+ kind: "atom"
124085
+ });
124086
+ return offset2 + 1;
124087
+ }
124088
+ return this.#walkNodeContent(node2, docPos + 1, offset2, addSegment);
124089
+ }
124090
+ /**
124091
+ * Mark the index as stale. It will be rebuilt on next search.
124092
+ */
124093
+ invalidate() {
124094
+ this.valid = false;
124095
+ }
124096
+ /**
124097
+ * Check if the index needs rebuilding for the given document.
124098
+ *
124099
+ * @param {import('prosemirror-model').Node} doc - The document to check against
124100
+ * @returns {boolean} True if index is stale and needs rebuilding
124101
+ */
124102
+ isStale(doc2) {
124103
+ return !this.valid || doc2.content.size !== this.docSize;
124104
+ }
124105
+ /**
124106
+ * Ensure the index is valid for the given document.
124107
+ * Rebuilds if stale.
124108
+ *
124109
+ * @param {import('prosemirror-model').Node} doc - The document
124110
+ */
124111
+ ensureValid(doc2) {
124112
+ if (this.isStale(doc2)) {
124113
+ this.build(doc2);
124114
+ }
124115
+ }
124116
+ /**
124117
+ * Convert an offset range in the flattened string to document ranges.
124118
+ * Skips separator/atom segments and returns only text ranges.
124119
+ *
124120
+ * @param {number} start - Start offset in flattened string
124121
+ * @param {number} end - End offset in flattened string
124122
+ * @returns {DocRange[]} Array of document ranges (text segments only)
124123
+ */
124124
+ offsetRangeToDocRanges(start2, end2) {
124125
+ const ranges = [];
124126
+ for (const segment of this.segments) {
124127
+ if (segment.offsetEnd <= start2) continue;
124128
+ if (segment.offsetStart >= end2) break;
124129
+ if (segment.kind !== "text") continue;
124130
+ const overlapStart = Math.max(start2, segment.offsetStart);
124131
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
124132
+ if (overlapStart < overlapEnd) {
124133
+ const startInSegment = overlapStart - segment.offsetStart;
124134
+ const endInSegment = overlapEnd - segment.offsetStart;
124135
+ ranges.push({
124136
+ from: segment.docFrom + startInSegment,
124137
+ to: segment.docFrom + endInSegment
124138
+ });
124139
+ }
124140
+ }
124141
+ return ranges;
124142
+ }
124143
+ /**
124144
+ * Find the document position for a given offset in the flattened string.
124145
+ *
124146
+ * @param {number} offset - Offset in flattened string
124147
+ * @returns {number|null} Document position, or null if not found
124148
+ */
124149
+ offsetToDocPos(offset2) {
124150
+ for (const segment of this.segments) {
124151
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
124152
+ if (segment.kind === "text") {
124153
+ return segment.docFrom + (offset2 - segment.offsetStart);
124154
+ }
124155
+ return segment.docFrom;
124156
+ }
124157
+ }
124158
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
124159
+ const lastSeg = this.segments[this.segments.length - 1];
124160
+ return lastSeg.docTo;
124161
+ }
124162
+ return null;
124163
+ }
124164
+ /**
124165
+ * Escape special regex characters in a string.
124166
+ *
124167
+ * @param {string} str - String to escape
124168
+ * @returns {string} Escaped string safe for use in RegExp
124169
+ */
124170
+ static escapeRegex(str) {
124171
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
124172
+ }
124173
+ /**
124174
+ * Convert a plain search string to a whitespace-flexible regex pattern.
124175
+ * This allows matching across paragraph boundaries.
124176
+ *
124177
+ * @param {string} searchString - The search string
124178
+ * @returns {string} Regex pattern string
124179
+ */
124180
+ static toFlexiblePattern(searchString) {
124181
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
124182
+ if (parts.length === 0) return "";
124183
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
124184
+ }
124185
+ /**
124186
+ * Search the index for matches.
124187
+ *
124188
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
124189
+ * @param {Object} options - Search options
124190
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
124191
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
124192
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
124193
+ */
124194
+ search(pattern, options = {}) {
124195
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
124196
+ const matches2 = [];
124197
+ let regex;
124198
+ if (pattern instanceof RegExp) {
124199
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
124200
+ regex = new RegExp(pattern.source, flags);
124201
+ } else if (typeof pattern === "string") {
124202
+ if (pattern.length === 0) return matches2;
124203
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
124204
+ if (flexiblePattern.length === 0) return matches2;
124205
+ const flags = caseSensitive ? "g" : "gi";
124206
+ regex = new RegExp(flexiblePattern, flags);
124207
+ } else {
124208
+ return matches2;
124209
+ }
124210
+ let match;
124211
+ while ((match = regex.exec(this.text)) !== null && matches2.length < maxMatches) {
124212
+ matches2.push({
124213
+ start: match.index,
124214
+ end: match.index + match[0].length,
124215
+ text: match[0]
124216
+ });
124217
+ if (match[0].length === 0) {
124218
+ regex.lastIndex++;
124219
+ }
124220
+ }
124221
+ return matches2;
123807
124222
  }
123808
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
123809
- return tr.setMeta(searchKey, { query, range: range2, highlight });
123810
124223
  }
124224
+ const customSearchHighlightsKey = new PluginKey("customSearchHighlights");
123811
124225
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
124226
+ const resolveInlineTextPosition = (doc2, position2, direction) => {
124227
+ const docSize = doc2.content.size;
124228
+ if (!Number.isFinite(position2) || position2 < 0 || position2 > docSize) {
124229
+ return position2;
124230
+ }
124231
+ const step = direction === "forward" ? 1 : -1;
124232
+ let current = position2;
124233
+ let iterations = 0;
124234
+ while (iterations < 8) {
124235
+ iterations += 1;
124236
+ const resolved = doc2.resolve(current);
124237
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
124238
+ if (!boundaryNode) break;
124239
+ if (boundaryNode.isText) break;
124240
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
124241
+ const next2 = current + step;
124242
+ if (next2 < 0 || next2 > docSize) break;
124243
+ current = next2;
124244
+ const adjacent = doc2.resolve(current);
124245
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
124246
+ if (checkNode && checkNode.isText) break;
124247
+ }
124248
+ return current;
124249
+ };
124250
+ const resolveSearchRange = ({ doc: doc2, from: from2, to, expectedText, highlights }) => {
124251
+ const docSize = doc2.content.size;
124252
+ let resolvedFrom = Math.max(0, Math.min(from2, docSize));
124253
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
124254
+ if (highlights) {
124255
+ const windowStart = Math.max(0, resolvedFrom - 4);
124256
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
124257
+ const candidates = highlights.find(windowStart, windowEnd);
124258
+ if (candidates.length > 0) {
124259
+ let chosen = candidates[0];
124260
+ if (expectedText) {
124261
+ const matching = candidates.filter(
124262
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
124263
+ );
124264
+ if (matching.length > 0) {
124265
+ chosen = matching[0];
124266
+ }
124267
+ }
124268
+ resolvedFrom = chosen.from;
124269
+ resolvedTo = chosen.to;
124270
+ }
124271
+ }
124272
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
124273
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
124274
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
124275
+ resolvedFrom = normalizedFrom;
124276
+ resolvedTo = normalizedTo;
124277
+ }
124278
+ return { from: resolvedFrom, to: resolvedTo };
124279
+ };
124280
+ const getPositionTracker = (editor) => {
124281
+ if (!editor) return null;
124282
+ if (editor.positionTracker) return editor.positionTracker;
124283
+ const storageTracker = editor.storage?.positionTracker?.tracker;
124284
+ if (storageTracker) {
124285
+ editor.positionTracker = storageTracker;
124286
+ return storageTracker;
124287
+ }
124288
+ const tracker = new PositionTracker(editor);
124289
+ if (editor.storage?.positionTracker) {
124290
+ editor.storage.positionTracker.tracker = tracker;
124291
+ }
124292
+ editor.positionTracker = tracker;
124293
+ return tracker;
124294
+ };
123812
124295
  const Search = Extension.create({
123813
124296
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
123814
124297
  addStorage() {
@@ -123817,29 +124300,58 @@ ${o}
123817
124300
  * @private
123818
124301
  * @type {SearchMatch[]|null}
123819
124302
  */
123820
- searchResults: []
124303
+ searchResults: [],
124304
+ /**
124305
+ * @private
124306
+ * @type {boolean}
124307
+ * Whether to apply CSS highlight classes to matches
124308
+ */
124309
+ highlightEnabled: true,
124310
+ /**
124311
+ * @private
124312
+ * @type {SearchIndex}
124313
+ * Lazily-built search index for cross-paragraph matching
124314
+ */
124315
+ searchIndex: new SearchIndex()
123821
124316
  };
123822
124317
  },
123823
124318
  addPmPlugins() {
123824
124319
  const editor = this.editor;
123825
124320
  const storage = this.storage;
124321
+ const searchIndexInvalidatorPlugin = new Plugin({
124322
+ key: new PluginKey("searchIndexInvalidator"),
124323
+ appendTransaction(transactions, oldState, newState) {
124324
+ const docChanged = transactions.some((tr) => tr.docChanged);
124325
+ if (docChanged && storage?.searchIndex) {
124326
+ storage.searchIndex.invalidate();
124327
+ }
124328
+ return null;
124329
+ }
124330
+ });
123826
124331
  const searchHighlightWithIdPlugin = new Plugin({
123827
- key: new PluginKey("customSearchHighlights"),
124332
+ key: customSearchHighlightsKey,
123828
124333
  props: {
123829
124334
  decorations(state) {
123830
124335
  if (!editor) return null;
123831
124336
  const matches2 = storage?.searchResults;
123832
124337
  if (!matches2?.length) return null;
123833
- const decorations = matches2.map(
123834
- (match) => Decoration.inline(match.from, match.to, {
123835
- id: `search-match-${match.id}`
123836
- })
123837
- );
124338
+ const highlightEnabled = storage?.highlightEnabled !== false;
124339
+ const decorations = [];
124340
+ for (const match of matches2) {
124341
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
124342
+ if (match.ranges && match.ranges.length > 0) {
124343
+ for (const range2 of match.ranges) {
124344
+ decorations.push(Decoration.inline(range2.from, range2.to, attrs));
124345
+ }
124346
+ } else {
124347
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
124348
+ }
124349
+ }
123838
124350
  return DecorationSet.create(state.doc, decorations);
123839
124351
  }
123840
124352
  }
123841
124353
  });
123842
- return [search$1(), searchHighlightWithIdPlugin];
124354
+ return [search$1(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
123843
124355
  },
123844
124356
  addCommands() {
123845
124357
  return {
@@ -123853,21 +124365,51 @@ ${o}
123853
124365
  goToFirstMatch: () => (
123854
124366
  /** @returns {boolean} */
123855
124367
  ({ state, editor, dispatch }) => {
124368
+ const searchResults = this.storage?.searchResults;
124369
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
124370
+ const firstMatch = searchResults[0];
124371
+ const from2 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
124372
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
124373
+ if (typeof from2 !== "number" || typeof to !== "number") {
124374
+ return false;
124375
+ }
124376
+ editor.view.focus();
124377
+ const tr2 = state.tr.setSelection(TextSelection$1.create(state.doc, from2, to)).scrollIntoView();
124378
+ if (dispatch) dispatch(tr2);
124379
+ const presentationEditor2 = editor.presentationEditor;
124380
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
124381
+ const didScroll = presentationEditor2.scrollToPosition(from2, { block: "center" });
124382
+ if (didScroll) return true;
124383
+ }
124384
+ try {
124385
+ const domPos = editor.view.domAtPos(from2);
124386
+ if (domPos?.node?.scrollIntoView) {
124387
+ domPos.node.scrollIntoView(true);
124388
+ }
124389
+ } catch {
124390
+ }
124391
+ return true;
124392
+ }
123856
124393
  const highlights = getMatchHighlights(state);
123857
124394
  if (!highlights) return false;
123858
124395
  const decorations = highlights.find();
123859
124396
  if (!decorations?.length) return false;
123860
- const firstMatch = decorations[0];
124397
+ const firstDeco = decorations[0];
123861
124398
  editor.view.focus();
123862
- const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
124399
+ const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
123863
124400
  if (dispatch) dispatch(tr);
123864
124401
  const presentationEditor = editor.presentationEditor;
123865
124402
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
123866
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
124403
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
123867
124404
  if (didScroll) return true;
123868
124405
  }
123869
- const domPos = editor.view.domAtPos(firstMatch.from);
123870
- domPos?.node?.scrollIntoView(true);
124406
+ try {
124407
+ const domPos = editor.view.domAtPos(firstDeco.from);
124408
+ if (domPos?.node?.scrollIntoView) {
124409
+ domPos.node.scrollIntoView(true);
124410
+ }
124411
+ } catch {
124412
+ }
123871
124413
  return true;
123872
124414
  }
123873
124415
  ),
@@ -123885,53 +124427,57 @@ ${o}
123885
124427
  *
123886
124428
  * // Search without visual highlighting
123887
124429
  * const silentMatches = editor.commands.search('test', { highlight: false })
123888
- * @note Returns array of SearchMatch objects with positions and IDs
124430
+ *
124431
+ * // Cross-paragraph search (works by default for plain strings)
124432
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
124433
+ * @note Returns array of SearchMatch objects with positions and IDs.
124434
+ * Plain string searches are whitespace-flexible and match across paragraphs.
124435
+ * Regex searches match exactly as specified.
123889
124436
  */
123890
124437
  search: (patternInput, options = {}) => (
123891
124438
  /** @returns {SearchMatch[]} */
123892
- ({ state, dispatch }) => {
124439
+ ({ state, dispatch, editor }) => {
123893
124440
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
123894
124441
  throw new TypeError("Search options must be an object");
123895
124442
  }
123896
124443
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
123897
- let pattern;
124444
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
123898
124445
  let caseSensitive = false;
123899
- let regexp = false;
123900
- const wholeWord = false;
124446
+ let searchPattern = patternInput;
123901
124447
  if (isRegExp(patternInput)) {
123902
- const regexPattern = (
123903
- /** @type {RegExp} */
123904
- patternInput
123905
- );
123906
- regexp = true;
123907
- pattern = regexPattern.source;
123908
- caseSensitive = !regexPattern.flags.includes("i");
124448
+ caseSensitive = !patternInput.flags.includes("i");
124449
+ searchPattern = patternInput;
123909
124450
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
123910
124451
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
123911
- regexp = true;
123912
- pattern = body;
123913
124452
  caseSensitive = !flags.includes("i");
124453
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
123914
124454
  } else {
123915
- pattern = String(patternInput);
124455
+ searchPattern = String(patternInput);
123916
124456
  }
123917
- const query = new SearchQuery({
123918
- search: pattern,
124457
+ const searchIndex = this.storage.searchIndex;
124458
+ searchIndex.ensureValid(state.doc);
124459
+ const indexMatches = searchIndex.search(searchPattern, {
123919
124460
  caseSensitive,
123920
- regexp,
123921
- wholeWord
124461
+ maxMatches
123922
124462
  });
123923
- const tr = setSearchState(state.tr, query, null, { highlight });
123924
- dispatch(tr);
123925
- const newState = state.apply(tr);
123926
- const decoSet = getMatchHighlights(newState);
123927
- const matches2 = decoSet ? decoSet.find() : [];
123928
- const resultMatches = matches2.map((d2) => ({
123929
- from: d2.from,
123930
- to: d2.to,
123931
- text: newState.doc.textBetween(d2.from, d2.to),
123932
- id: v4()
123933
- }));
124463
+ const resultMatches = [];
124464
+ for (const indexMatch of indexMatches) {
124465
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
124466
+ if (ranges.length === 0) continue;
124467
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
124468
+ const combinedText = matchTexts.join("");
124469
+ const match = {
124470
+ from: ranges[0].from,
124471
+ to: ranges[ranges.length - 1].to,
124472
+ text: combinedText,
124473
+ id: v4(),
124474
+ ranges,
124475
+ trackerIds: []
124476
+ };
124477
+ resultMatches.push(match);
124478
+ }
123934
124479
  this.storage.searchResults = resultMatches;
124480
+ this.storage.highlightEnabled = highlight;
123935
124481
  return resultMatches;
123936
124482
  }
123937
124483
  ),
@@ -123942,12 +124488,48 @@ ${o}
123942
124488
  * @example
123943
124489
  * const searchResults = editor.commands.search('test string')
123944
124490
  * editor.commands.goToSearchResult(searchResults[3])
123945
- * @note Scrolls to match and selects it
124491
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
124492
+ * selects the first range and scrolls to it.
123946
124493
  */
123947
124494
  goToSearchResult: (match) => (
123948
124495
  /** @returns {boolean} */
123949
124496
  ({ state, dispatch, editor }) => {
123950
- const { from: from2, to } = match;
124497
+ const positionTracker = getPositionTracker(editor);
124498
+ const doc2 = state.doc;
124499
+ const highlights = getMatchHighlights(state);
124500
+ let from2, to;
124501
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
124502
+ if (positionTracker?.resolve && match.trackerIds[0]) {
124503
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
124504
+ if (resolved) {
124505
+ from2 = resolved.from;
124506
+ to = resolved.to;
124507
+ }
124508
+ }
124509
+ if (from2 === void 0) {
124510
+ from2 = match.ranges[0].from;
124511
+ to = match.ranges[0].to;
124512
+ }
124513
+ } else {
124514
+ from2 = match.from;
124515
+ to = match.to;
124516
+ if (positionTracker?.resolve && match?.id) {
124517
+ const resolved = positionTracker.resolve(match.id);
124518
+ if (resolved) {
124519
+ from2 = resolved.from;
124520
+ to = resolved.to;
124521
+ }
124522
+ }
124523
+ }
124524
+ const normalized = resolveSearchRange({
124525
+ doc: doc2,
124526
+ from: from2,
124527
+ to,
124528
+ expectedText: match?.text ?? null,
124529
+ highlights
124530
+ });
124531
+ from2 = normalized.from;
124532
+ to = normalized.to;
123951
124533
  editor.view.focus();
123952
124534
  const tr = state.tr.setSelection(TextSelection$1.create(state.doc, from2, to)).scrollIntoView();
123953
124535
  if (dispatch) dispatch(tr);
@@ -149687,7 +150269,7 @@ ${reason}`);
149687
150269
  this.config.colors = shuffleArray(this.config.colors);
149688
150270
  this.userColorMap = /* @__PURE__ */ new Map();
149689
150271
  this.colorIndex = 0;
149690
- this.version = "1.6.0-next.1";
150272
+ this.version = "1.6.0-next.2";
149691
150273
  this.#log("🦋 [superdoc] Using SuperDoc version:", this.version);
149692
150274
  this.superdocId = config2.superdocId || v4();
149693
150275
  this.colors = this.config.colors;