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

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.3") {
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
  /**
@@ -51085,6 +51265,90 @@ Please report this to https://github.com/markedjs/marked.`, e) {
51085
51265
  name: "comments",
51086
51266
  addCommands() {
51087
51267
  return {
51268
+ /**
51269
+ * Add a comment to the current selection
51270
+ * @category Command
51271
+ * @param {string|Object} contentOrOptions - Comment content as a string, or an options object
51272
+ * @param {string} [contentOrOptions.content] - The comment content (text or HTML)
51273
+ * @param {string} [contentOrOptions.author] - Author name (defaults to user from editor config)
51274
+ * @param {string} [contentOrOptions.authorEmail] - Author email (defaults to user from editor config)
51275
+ * @param {string} [contentOrOptions.authorImage] - Author image URL (defaults to user from editor config)
51276
+ * @param {boolean} [contentOrOptions.isInternal=false] - Whether the comment is internal/private
51277
+ * @returns {boolean} True if the comment was added successfully, false otherwise
51278
+ * @example
51279
+ * // Simple usage with just content
51280
+ * editor.commands.addComment('This needs review')
51281
+ *
51282
+ * // With options
51283
+ * editor.commands.addComment({
51284
+ * content: 'Please clarify this section',
51285
+ * author: 'Jane Doe',
51286
+ * isInternal: true
51287
+ * })
51288
+ *
51289
+ * // To get the comment ID, listen to the commentsUpdate event
51290
+ * editor.on('commentsUpdate', (event) => {
51291
+ * if (event.type === 'add') {
51292
+ * console.log('New comment ID:', event.activeCommentId)
51293
+ * }
51294
+ * })
51295
+ */
51296
+ addComment: (contentOrOptions) => ({ tr, dispatch, editor }) => {
51297
+ const { selection } = tr;
51298
+ const { $from, $to } = selection;
51299
+ if ($from.pos === $to.pos) {
51300
+ console.warn("addComment requires a text selection. Please select text before adding a comment.");
51301
+ return false;
51302
+ }
51303
+ let content2, author, authorEmail, authorImage, isInternal;
51304
+ if (typeof contentOrOptions === "string") {
51305
+ content2 = contentOrOptions;
51306
+ } else if (contentOrOptions && typeof contentOrOptions === "object") {
51307
+ content2 = contentOrOptions.content;
51308
+ author = contentOrOptions.author;
51309
+ authorEmail = contentOrOptions.authorEmail;
51310
+ authorImage = contentOrOptions.authorImage;
51311
+ isInternal = contentOrOptions.isInternal;
51312
+ }
51313
+ const commentId = v4();
51314
+ const resolvedInternal = isInternal ?? false;
51315
+ const configUser = editor.options?.user || {};
51316
+ tr.setMeta(CommentsPluginKey, { event: "add" });
51317
+ tr.addMark(
51318
+ $from.pos,
51319
+ $to.pos,
51320
+ editor.schema.marks[CommentMarkName$1].create({
51321
+ commentId,
51322
+ internal: resolvedInternal
51323
+ })
51324
+ );
51325
+ if (dispatch) dispatch(tr);
51326
+ const commentPayload = normalizeCommentEventPayload({
51327
+ conversation: {
51328
+ commentId,
51329
+ isInternal: resolvedInternal,
51330
+ commentText: content2,
51331
+ creatorName: author ?? configUser.name,
51332
+ creatorEmail: authorEmail ?? configUser.email,
51333
+ creatorImage: authorImage ?? configUser.image,
51334
+ createdTime: Date.now()
51335
+ },
51336
+ editorOptions: editor.options,
51337
+ fallbackCommentId: commentId,
51338
+ fallbackInternal: resolvedInternal
51339
+ });
51340
+ editor.emit("commentsUpdate", {
51341
+ type: comments_module_events.ADD,
51342
+ comment: commentPayload,
51343
+ activeCommentId: commentId
51344
+ });
51345
+ return true;
51346
+ },
51347
+ /**
51348
+ * @private
51349
+ * Internal command to insert a comment mark at the current selection.
51350
+ * Use `addComment` for the public API.
51351
+ */
51088
51352
  insertComment: (conversation = {}) => ({ tr, dispatch }) => {
51089
51353
  const { selection } = tr;
51090
51354
  const { $from, $to } = selection;
@@ -64003,7 +64267,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
64003
64267
  return false;
64004
64268
  }
64005
64269
  };
64006
- const summaryVersion = "1.6.0-next.1";
64270
+ const summaryVersion = "1.6.0-next.3";
64007
64271
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
64008
64272
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
64009
64273
  function mapAttributes(attrs) {
@@ -65497,7 +65761,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
65497
65761
  */
65498
65762
  #createExtensionService() {
65499
65763
  const allowedExtensions = ["extension", "node", "mark"];
65500
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
65764
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
65501
65765
  const externalExtensions = this.options.externalExtensions || [];
65502
65766
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
65503
65767
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -66671,7 +66935,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
66671
66935
  * Process collaboration migrations
66672
66936
  */
66673
66937
  processCollaborationMigrations() {
66674
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
66938
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.3");
66675
66939
  if (!this.options.ydoc) return;
66676
66940
  const metaMap = this.options.ydoc.getMap("meta");
66677
66941
  let docVersion = metaMap.get("version");
@@ -123801,14 +124065,317 @@ ${o}
123801
124065
  let search2 = searchKey.getState(state);
123802
124066
  return search2 ? search2.deco : DecorationSet.empty;
123803
124067
  }
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");
124068
+ const BLOCK_SEPARATOR = "\n";
124069
+ const ATOM_PLACEHOLDER = "";
124070
+ class SearchIndex {
124071
+ /** @type {string} */
124072
+ text = "";
124073
+ /** @type {Segment[]} */
124074
+ segments = [];
124075
+ /** @type {boolean} */
124076
+ valid = false;
124077
+ /** @type {number} */
124078
+ docSize = 0;
124079
+ /**
124080
+ * Build the search index from a ProseMirror document.
124081
+ * Uses doc.textBetween for the flattened string and walks
124082
+ * the document to build the segment offset map.
124083
+ *
124084
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
124085
+ */
124086
+ build(doc2) {
124087
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
124088
+ this.segments = [];
124089
+ this.docSize = doc2.content.size;
124090
+ let offset2 = 0;
124091
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
124092
+ this.segments.push(segment);
124093
+ offset2 = segment.offsetEnd;
124094
+ });
124095
+ this.valid = true;
124096
+ }
124097
+ /**
124098
+ * Walk the content of a node to build segments.
124099
+ * This method processes the children of a node, given the position
124100
+ * where the node's content starts.
124101
+ *
124102
+ * @param {import('prosemirror-model').Node} node - Current node
124103
+ * @param {number} contentStart - Document position where this node's content starts
124104
+ * @param {number} offset - Current offset in flattened string
124105
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
124106
+ * @returns {number} The new offset after processing this node's content
124107
+ */
124108
+ #walkNodeContent(node2, contentStart, offset2, addSegment) {
124109
+ let currentOffset = offset2;
124110
+ let isFirstChild = true;
124111
+ node2.forEach((child, childContentOffset) => {
124112
+ const childDocPos = contentStart + childContentOffset;
124113
+ if (child.isBlock && !isFirstChild) {
124114
+ addSegment({
124115
+ offsetStart: currentOffset,
124116
+ offsetEnd: currentOffset + 1,
124117
+ docFrom: childDocPos,
124118
+ docTo: childDocPos,
124119
+ kind: "blockSep"
124120
+ });
124121
+ currentOffset += 1;
124122
+ }
124123
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
124124
+ isFirstChild = false;
124125
+ });
124126
+ return currentOffset;
124127
+ }
124128
+ /**
124129
+ * Recursively walk a node and its descendants to build segments.
124130
+ *
124131
+ * @param {import('prosemirror-model').Node} node - Current node
124132
+ * @param {number} docPos - Document position at start of this node
124133
+ * @param {number} offset - Current offset in flattened string
124134
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
124135
+ * @returns {number} The new offset after processing this node
124136
+ */
124137
+ #walkNode(node2, docPos, offset2, addSegment) {
124138
+ if (node2.isText) {
124139
+ const text2 = node2.text || "";
124140
+ if (text2.length > 0) {
124141
+ addSegment({
124142
+ offsetStart: offset2,
124143
+ offsetEnd: offset2 + text2.length,
124144
+ docFrom: docPos,
124145
+ docTo: docPos + text2.length,
124146
+ kind: "text"
124147
+ });
124148
+ return offset2 + text2.length;
124149
+ }
124150
+ return offset2;
124151
+ }
124152
+ if (node2.isLeaf) {
124153
+ if (node2.type.name === "hard_break") {
124154
+ addSegment({
124155
+ offsetStart: offset2,
124156
+ offsetEnd: offset2 + 1,
124157
+ docFrom: docPos,
124158
+ docTo: docPos + node2.nodeSize,
124159
+ kind: "hardBreak"
124160
+ });
124161
+ return offset2 + 1;
124162
+ }
124163
+ addSegment({
124164
+ offsetStart: offset2,
124165
+ offsetEnd: offset2 + 1,
124166
+ docFrom: docPos,
124167
+ docTo: docPos + node2.nodeSize,
124168
+ kind: "atom"
124169
+ });
124170
+ return offset2 + 1;
124171
+ }
124172
+ return this.#walkNodeContent(node2, docPos + 1, offset2, addSegment);
124173
+ }
124174
+ /**
124175
+ * Mark the index as stale. It will be rebuilt on next search.
124176
+ */
124177
+ invalidate() {
124178
+ this.valid = false;
124179
+ }
124180
+ /**
124181
+ * Check if the index needs rebuilding for the given document.
124182
+ *
124183
+ * @param {import('prosemirror-model').Node} doc - The document to check against
124184
+ * @returns {boolean} True if index is stale and needs rebuilding
124185
+ */
124186
+ isStale(doc2) {
124187
+ return !this.valid || doc2.content.size !== this.docSize;
124188
+ }
124189
+ /**
124190
+ * Ensure the index is valid for the given document.
124191
+ * Rebuilds if stale.
124192
+ *
124193
+ * @param {import('prosemirror-model').Node} doc - The document
124194
+ */
124195
+ ensureValid(doc2) {
124196
+ if (this.isStale(doc2)) {
124197
+ this.build(doc2);
124198
+ }
124199
+ }
124200
+ /**
124201
+ * Convert an offset range in the flattened string to document ranges.
124202
+ * Skips separator/atom segments and returns only text ranges.
124203
+ *
124204
+ * @param {number} start - Start offset in flattened string
124205
+ * @param {number} end - End offset in flattened string
124206
+ * @returns {DocRange[]} Array of document ranges (text segments only)
124207
+ */
124208
+ offsetRangeToDocRanges(start2, end2) {
124209
+ const ranges = [];
124210
+ for (const segment of this.segments) {
124211
+ if (segment.offsetEnd <= start2) continue;
124212
+ if (segment.offsetStart >= end2) break;
124213
+ if (segment.kind !== "text") continue;
124214
+ const overlapStart = Math.max(start2, segment.offsetStart);
124215
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
124216
+ if (overlapStart < overlapEnd) {
124217
+ const startInSegment = overlapStart - segment.offsetStart;
124218
+ const endInSegment = overlapEnd - segment.offsetStart;
124219
+ ranges.push({
124220
+ from: segment.docFrom + startInSegment,
124221
+ to: segment.docFrom + endInSegment
124222
+ });
124223
+ }
124224
+ }
124225
+ return ranges;
124226
+ }
124227
+ /**
124228
+ * Find the document position for a given offset in the flattened string.
124229
+ *
124230
+ * @param {number} offset - Offset in flattened string
124231
+ * @returns {number|null} Document position, or null if not found
124232
+ */
124233
+ offsetToDocPos(offset2) {
124234
+ for (const segment of this.segments) {
124235
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
124236
+ if (segment.kind === "text") {
124237
+ return segment.docFrom + (offset2 - segment.offsetStart);
124238
+ }
124239
+ return segment.docFrom;
124240
+ }
124241
+ }
124242
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
124243
+ const lastSeg = this.segments[this.segments.length - 1];
124244
+ return lastSeg.docTo;
124245
+ }
124246
+ return null;
124247
+ }
124248
+ /**
124249
+ * Escape special regex characters in a string.
124250
+ *
124251
+ * @param {string} str - String to escape
124252
+ * @returns {string} Escaped string safe for use in RegExp
124253
+ */
124254
+ static escapeRegex(str) {
124255
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
124256
+ }
124257
+ /**
124258
+ * Convert a plain search string to a whitespace-flexible regex pattern.
124259
+ * This allows matching across paragraph boundaries.
124260
+ *
124261
+ * @param {string} searchString - The search string
124262
+ * @returns {string} Regex pattern string
124263
+ */
124264
+ static toFlexiblePattern(searchString) {
124265
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
124266
+ if (parts.length === 0) return "";
124267
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
124268
+ }
124269
+ /**
124270
+ * Search the index for matches.
124271
+ *
124272
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
124273
+ * @param {Object} options - Search options
124274
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
124275
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
124276
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
124277
+ */
124278
+ search(pattern, options = {}) {
124279
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
124280
+ const matches2 = [];
124281
+ let regex;
124282
+ if (pattern instanceof RegExp) {
124283
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
124284
+ regex = new RegExp(pattern.source, flags);
124285
+ } else if (typeof pattern === "string") {
124286
+ if (pattern.length === 0) return matches2;
124287
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
124288
+ if (flexiblePattern.length === 0) return matches2;
124289
+ const flags = caseSensitive ? "g" : "gi";
124290
+ regex = new RegExp(flexiblePattern, flags);
124291
+ } else {
124292
+ return matches2;
124293
+ }
124294
+ let match;
124295
+ while ((match = regex.exec(this.text)) !== null && matches2.length < maxMatches) {
124296
+ matches2.push({
124297
+ start: match.index,
124298
+ end: match.index + match[0].length,
124299
+ text: match[0]
124300
+ });
124301
+ if (match[0].length === 0) {
124302
+ regex.lastIndex++;
124303
+ }
124304
+ }
124305
+ return matches2;
123807
124306
  }
123808
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
123809
- return tr.setMeta(searchKey, { query, range: range2, highlight });
123810
124307
  }
124308
+ const customSearchHighlightsKey = new PluginKey("customSearchHighlights");
123811
124309
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
124310
+ const resolveInlineTextPosition = (doc2, position2, direction) => {
124311
+ const docSize = doc2.content.size;
124312
+ if (!Number.isFinite(position2) || position2 < 0 || position2 > docSize) {
124313
+ return position2;
124314
+ }
124315
+ const step = direction === "forward" ? 1 : -1;
124316
+ let current = position2;
124317
+ let iterations = 0;
124318
+ while (iterations < 8) {
124319
+ iterations += 1;
124320
+ const resolved = doc2.resolve(current);
124321
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
124322
+ if (!boundaryNode) break;
124323
+ if (boundaryNode.isText) break;
124324
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
124325
+ const next2 = current + step;
124326
+ if (next2 < 0 || next2 > docSize) break;
124327
+ current = next2;
124328
+ const adjacent = doc2.resolve(current);
124329
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
124330
+ if (checkNode && checkNode.isText) break;
124331
+ }
124332
+ return current;
124333
+ };
124334
+ const resolveSearchRange = ({ doc: doc2, from: from2, to, expectedText, highlights }) => {
124335
+ const docSize = doc2.content.size;
124336
+ let resolvedFrom = Math.max(0, Math.min(from2, docSize));
124337
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
124338
+ if (highlights) {
124339
+ const windowStart = Math.max(0, resolvedFrom - 4);
124340
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
124341
+ const candidates = highlights.find(windowStart, windowEnd);
124342
+ if (candidates.length > 0) {
124343
+ let chosen = candidates[0];
124344
+ if (expectedText) {
124345
+ const matching = candidates.filter(
124346
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
124347
+ );
124348
+ if (matching.length > 0) {
124349
+ chosen = matching[0];
124350
+ }
124351
+ }
124352
+ resolvedFrom = chosen.from;
124353
+ resolvedTo = chosen.to;
124354
+ }
124355
+ }
124356
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
124357
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
124358
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
124359
+ resolvedFrom = normalizedFrom;
124360
+ resolvedTo = normalizedTo;
124361
+ }
124362
+ return { from: resolvedFrom, to: resolvedTo };
124363
+ };
124364
+ const getPositionTracker = (editor) => {
124365
+ if (!editor) return null;
124366
+ if (editor.positionTracker) return editor.positionTracker;
124367
+ const storageTracker = editor.storage?.positionTracker?.tracker;
124368
+ if (storageTracker) {
124369
+ editor.positionTracker = storageTracker;
124370
+ return storageTracker;
124371
+ }
124372
+ const tracker = new PositionTracker(editor);
124373
+ if (editor.storage?.positionTracker) {
124374
+ editor.storage.positionTracker.tracker = tracker;
124375
+ }
124376
+ editor.positionTracker = tracker;
124377
+ return tracker;
124378
+ };
123812
124379
  const Search = Extension.create({
123813
124380
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
123814
124381
  addStorage() {
@@ -123817,29 +124384,58 @@ ${o}
123817
124384
  * @private
123818
124385
  * @type {SearchMatch[]|null}
123819
124386
  */
123820
- searchResults: []
124387
+ searchResults: [],
124388
+ /**
124389
+ * @private
124390
+ * @type {boolean}
124391
+ * Whether to apply CSS highlight classes to matches
124392
+ */
124393
+ highlightEnabled: true,
124394
+ /**
124395
+ * @private
124396
+ * @type {SearchIndex}
124397
+ * Lazily-built search index for cross-paragraph matching
124398
+ */
124399
+ searchIndex: new SearchIndex()
123821
124400
  };
123822
124401
  },
123823
124402
  addPmPlugins() {
123824
124403
  const editor = this.editor;
123825
124404
  const storage = this.storage;
124405
+ const searchIndexInvalidatorPlugin = new Plugin({
124406
+ key: new PluginKey("searchIndexInvalidator"),
124407
+ appendTransaction(transactions, oldState, newState) {
124408
+ const docChanged = transactions.some((tr) => tr.docChanged);
124409
+ if (docChanged && storage?.searchIndex) {
124410
+ storage.searchIndex.invalidate();
124411
+ }
124412
+ return null;
124413
+ }
124414
+ });
123826
124415
  const searchHighlightWithIdPlugin = new Plugin({
123827
- key: new PluginKey("customSearchHighlights"),
124416
+ key: customSearchHighlightsKey,
123828
124417
  props: {
123829
124418
  decorations(state) {
123830
124419
  if (!editor) return null;
123831
124420
  const matches2 = storage?.searchResults;
123832
124421
  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
- );
124422
+ const highlightEnabled = storage?.highlightEnabled !== false;
124423
+ const decorations = [];
124424
+ for (const match of matches2) {
124425
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
124426
+ if (match.ranges && match.ranges.length > 0) {
124427
+ for (const range2 of match.ranges) {
124428
+ decorations.push(Decoration.inline(range2.from, range2.to, attrs));
124429
+ }
124430
+ } else {
124431
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
124432
+ }
124433
+ }
123838
124434
  return DecorationSet.create(state.doc, decorations);
123839
124435
  }
123840
124436
  }
123841
124437
  });
123842
- return [search$1(), searchHighlightWithIdPlugin];
124438
+ return [search$1(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
123843
124439
  },
123844
124440
  addCommands() {
123845
124441
  return {
@@ -123853,21 +124449,51 @@ ${o}
123853
124449
  goToFirstMatch: () => (
123854
124450
  /** @returns {boolean} */
123855
124451
  ({ state, editor, dispatch }) => {
124452
+ const searchResults = this.storage?.searchResults;
124453
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
124454
+ const firstMatch = searchResults[0];
124455
+ const from2 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
124456
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
124457
+ if (typeof from2 !== "number" || typeof to !== "number") {
124458
+ return false;
124459
+ }
124460
+ editor.view.focus();
124461
+ const tr2 = state.tr.setSelection(TextSelection$1.create(state.doc, from2, to)).scrollIntoView();
124462
+ if (dispatch) dispatch(tr2);
124463
+ const presentationEditor2 = editor.presentationEditor;
124464
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
124465
+ const didScroll = presentationEditor2.scrollToPosition(from2, { block: "center" });
124466
+ if (didScroll) return true;
124467
+ }
124468
+ try {
124469
+ const domPos = editor.view.domAtPos(from2);
124470
+ if (domPos?.node?.scrollIntoView) {
124471
+ domPos.node.scrollIntoView(true);
124472
+ }
124473
+ } catch {
124474
+ }
124475
+ return true;
124476
+ }
123856
124477
  const highlights = getMatchHighlights(state);
123857
124478
  if (!highlights) return false;
123858
124479
  const decorations = highlights.find();
123859
124480
  if (!decorations?.length) return false;
123860
- const firstMatch = decorations[0];
124481
+ const firstDeco = decorations[0];
123861
124482
  editor.view.focus();
123862
- const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
124483
+ const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
123863
124484
  if (dispatch) dispatch(tr);
123864
124485
  const presentationEditor = editor.presentationEditor;
123865
124486
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
123866
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
124487
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
123867
124488
  if (didScroll) return true;
123868
124489
  }
123869
- const domPos = editor.view.domAtPos(firstMatch.from);
123870
- domPos?.node?.scrollIntoView(true);
124490
+ try {
124491
+ const domPos = editor.view.domAtPos(firstDeco.from);
124492
+ if (domPos?.node?.scrollIntoView) {
124493
+ domPos.node.scrollIntoView(true);
124494
+ }
124495
+ } catch {
124496
+ }
123871
124497
  return true;
123872
124498
  }
123873
124499
  ),
@@ -123885,53 +124511,57 @@ ${o}
123885
124511
  *
123886
124512
  * // Search without visual highlighting
123887
124513
  * const silentMatches = editor.commands.search('test', { highlight: false })
123888
- * @note Returns array of SearchMatch objects with positions and IDs
124514
+ *
124515
+ * // Cross-paragraph search (works by default for plain strings)
124516
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
124517
+ * @note Returns array of SearchMatch objects with positions and IDs.
124518
+ * Plain string searches are whitespace-flexible and match across paragraphs.
124519
+ * Regex searches match exactly as specified.
123889
124520
  */
123890
124521
  search: (patternInput, options = {}) => (
123891
124522
  /** @returns {SearchMatch[]} */
123892
- ({ state, dispatch }) => {
124523
+ ({ state, dispatch, editor }) => {
123893
124524
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
123894
124525
  throw new TypeError("Search options must be an object");
123895
124526
  }
123896
124527
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
123897
- let pattern;
124528
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
123898
124529
  let caseSensitive = false;
123899
- let regexp = false;
123900
- const wholeWord = false;
124530
+ let searchPattern = patternInput;
123901
124531
  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");
124532
+ caseSensitive = !patternInput.flags.includes("i");
124533
+ searchPattern = patternInput;
123909
124534
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
123910
124535
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
123911
- regexp = true;
123912
- pattern = body;
123913
124536
  caseSensitive = !flags.includes("i");
124537
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
123914
124538
  } else {
123915
- pattern = String(patternInput);
124539
+ searchPattern = String(patternInput);
123916
124540
  }
123917
- const query = new SearchQuery({
123918
- search: pattern,
124541
+ const searchIndex = this.storage.searchIndex;
124542
+ searchIndex.ensureValid(state.doc);
124543
+ const indexMatches = searchIndex.search(searchPattern, {
123919
124544
  caseSensitive,
123920
- regexp,
123921
- wholeWord
124545
+ maxMatches
123922
124546
  });
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
- }));
124547
+ const resultMatches = [];
124548
+ for (const indexMatch of indexMatches) {
124549
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
124550
+ if (ranges.length === 0) continue;
124551
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
124552
+ const combinedText = matchTexts.join("");
124553
+ const match = {
124554
+ from: ranges[0].from,
124555
+ to: ranges[ranges.length - 1].to,
124556
+ text: combinedText,
124557
+ id: v4(),
124558
+ ranges,
124559
+ trackerIds: []
124560
+ };
124561
+ resultMatches.push(match);
124562
+ }
123934
124563
  this.storage.searchResults = resultMatches;
124564
+ this.storage.highlightEnabled = highlight;
123935
124565
  return resultMatches;
123936
124566
  }
123937
124567
  ),
@@ -123942,12 +124572,48 @@ ${o}
123942
124572
  * @example
123943
124573
  * const searchResults = editor.commands.search('test string')
123944
124574
  * editor.commands.goToSearchResult(searchResults[3])
123945
- * @note Scrolls to match and selects it
124575
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
124576
+ * selects the first range and scrolls to it.
123946
124577
  */
123947
124578
  goToSearchResult: (match) => (
123948
124579
  /** @returns {boolean} */
123949
124580
  ({ state, dispatch, editor }) => {
123950
- const { from: from2, to } = match;
124581
+ const positionTracker = getPositionTracker(editor);
124582
+ const doc2 = state.doc;
124583
+ const highlights = getMatchHighlights(state);
124584
+ let from2, to;
124585
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
124586
+ if (positionTracker?.resolve && match.trackerIds[0]) {
124587
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
124588
+ if (resolved) {
124589
+ from2 = resolved.from;
124590
+ to = resolved.to;
124591
+ }
124592
+ }
124593
+ if (from2 === void 0) {
124594
+ from2 = match.ranges[0].from;
124595
+ to = match.ranges[0].to;
124596
+ }
124597
+ } else {
124598
+ from2 = match.from;
124599
+ to = match.to;
124600
+ if (positionTracker?.resolve && match?.id) {
124601
+ const resolved = positionTracker.resolve(match.id);
124602
+ if (resolved) {
124603
+ from2 = resolved.from;
124604
+ to = resolved.to;
124605
+ }
124606
+ }
124607
+ }
124608
+ const normalized = resolveSearchRange({
124609
+ doc: doc2,
124610
+ from: from2,
124611
+ to,
124612
+ expectedText: match?.text ?? null,
124613
+ highlights
124614
+ });
124615
+ from2 = normalized.from;
124616
+ to = normalized.to;
123951
124617
  editor.view.focus();
123952
124618
  const tr = state.tr.setSelection(TextSelection$1.create(state.doc, from2, to)).scrollIntoView();
123953
124619
  if (dispatch) dispatch(tr);
@@ -149687,7 +150353,7 @@ ${reason}`);
149687
150353
  this.config.colors = shuffleArray(this.config.colors);
149688
150354
  this.userColorMap = /* @__PURE__ */ new Map();
149689
150355
  this.colorIndex = 0;
149690
- this.version = "1.6.0-next.1";
150356
+ this.version = "1.6.0-next.3";
149691
150357
  this.#log("🦋 [superdoc] Using SuperDoc version:", this.version);
149692
150358
  this.superdocId = config2.superdocId || v4();
149693
150359
  this.colors = this.config.colors;