@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.
@@ -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-UL6mfKlt.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
  /**
@@ -13081,6 +13261,90 @@ const CommentsPlugin = Extension.create({
13081
13261
  name: "comments",
13082
13262
  addCommands() {
13083
13263
  return {
13264
+ /**
13265
+ * Add a comment to the current selection
13266
+ * @category Command
13267
+ * @param {string|Object} contentOrOptions - Comment content as a string, or an options object
13268
+ * @param {string} [contentOrOptions.content] - The comment content (text or HTML)
13269
+ * @param {string} [contentOrOptions.author] - Author name (defaults to user from editor config)
13270
+ * @param {string} [contentOrOptions.authorEmail] - Author email (defaults to user from editor config)
13271
+ * @param {string} [contentOrOptions.authorImage] - Author image URL (defaults to user from editor config)
13272
+ * @param {boolean} [contentOrOptions.isInternal=false] - Whether the comment is internal/private
13273
+ * @returns {boolean} True if the comment was added successfully, false otherwise
13274
+ * @example
13275
+ * // Simple usage with just content
13276
+ * editor.commands.addComment('This needs review')
13277
+ *
13278
+ * // With options
13279
+ * editor.commands.addComment({
13280
+ * content: 'Please clarify this section',
13281
+ * author: 'Jane Doe',
13282
+ * isInternal: true
13283
+ * })
13284
+ *
13285
+ * // To get the comment ID, listen to the commentsUpdate event
13286
+ * editor.on('commentsUpdate', (event) => {
13287
+ * if (event.type === 'add') {
13288
+ * console.log('New comment ID:', event.activeCommentId)
13289
+ * }
13290
+ * })
13291
+ */
13292
+ addComment: (contentOrOptions) => ({ tr, dispatch, editor }) => {
13293
+ const { selection } = tr;
13294
+ const { $from, $to } = selection;
13295
+ if ($from.pos === $to.pos) {
13296
+ console.warn("addComment requires a text selection. Please select text before adding a comment.");
13297
+ return false;
13298
+ }
13299
+ let content, author, authorEmail, authorImage, isInternal;
13300
+ if (typeof contentOrOptions === "string") {
13301
+ content = contentOrOptions;
13302
+ } else if (contentOrOptions && typeof contentOrOptions === "object") {
13303
+ content = contentOrOptions.content;
13304
+ author = contentOrOptions.author;
13305
+ authorEmail = contentOrOptions.authorEmail;
13306
+ authorImage = contentOrOptions.authorImage;
13307
+ isInternal = contentOrOptions.isInternal;
13308
+ }
13309
+ const commentId = uuid.v4();
13310
+ const resolvedInternal = isInternal ?? false;
13311
+ const configUser = editor.options?.user || {};
13312
+ tr.setMeta(CommentsPluginKey, { event: "add" });
13313
+ tr.addMark(
13314
+ $from.pos,
13315
+ $to.pos,
13316
+ editor.schema.marks[CommentMarkName$1].create({
13317
+ commentId,
13318
+ internal: resolvedInternal
13319
+ })
13320
+ );
13321
+ if (dispatch) dispatch(tr);
13322
+ const commentPayload = normalizeCommentEventPayload({
13323
+ conversation: {
13324
+ commentId,
13325
+ isInternal: resolvedInternal,
13326
+ commentText: content,
13327
+ creatorName: author ?? configUser.name,
13328
+ creatorEmail: authorEmail ?? configUser.email,
13329
+ creatorImage: authorImage ?? configUser.image,
13330
+ createdTime: Date.now()
13331
+ },
13332
+ editorOptions: editor.options,
13333
+ fallbackCommentId: commentId,
13334
+ fallbackInternal: resolvedInternal
13335
+ });
13336
+ editor.emit("commentsUpdate", {
13337
+ type: comments_module_events.ADD,
13338
+ comment: commentPayload,
13339
+ activeCommentId: commentId
13340
+ });
13341
+ return true;
13342
+ },
13343
+ /**
13344
+ * @private
13345
+ * Internal command to insert a comment mark at the current selection.
13346
+ * Use `addComment` for the public API.
13347
+ */
13084
13348
  insertComment: (conversation = {}) => ({ tr, dispatch }) => {
13085
13349
  const { selection } = tr;
13086
13350
  const { $from, $to } = selection;
@@ -15524,7 +15788,7 @@ const canUseDOM = () => {
15524
15788
  return false;
15525
15789
  }
15526
15790
  };
15527
- const summaryVersion = "1.6.0-next.1";
15791
+ const summaryVersion = "1.6.0-next.3";
15528
15792
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
15529
15793
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
15530
15794
  function mapAttributes(attrs) {
@@ -17017,7 +17281,7 @@ class Editor extends EventEmitter {
17017
17281
  */
17018
17282
  #createExtensionService() {
17019
17283
  const allowedExtensions = ["extension", "node", "mark"];
17020
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
17284
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
17021
17285
  const externalExtensions = this.options.externalExtensions || [];
17022
17286
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
17023
17287
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -18191,7 +18455,7 @@ class Editor extends EventEmitter {
18191
18455
  * Process collaboration migrations
18192
18456
  */
18193
18457
  processCollaborationMigrations() {
18194
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
18458
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.3");
18195
18459
  if (!this.options.ydoc) return;
18196
18460
  const metaMap = this.options.ydoc.getMap("meta");
18197
18461
  let docVersion = metaMap.get("version");
@@ -75544,14 +75808,317 @@ function getMatchHighlights(state) {
75544
75808
  let search2 = searchKey.getState(state);
75545
75809
  return search2 ? search2.deco : DecorationSet.empty;
75546
75810
  }
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");
75811
+ const BLOCK_SEPARATOR = "\n";
75812
+ const ATOM_PLACEHOLDER = "";
75813
+ class SearchIndex {
75814
+ /** @type {string} */
75815
+ text = "";
75816
+ /** @type {Segment[]} */
75817
+ segments = [];
75818
+ /** @type {boolean} */
75819
+ valid = false;
75820
+ /** @type {number} */
75821
+ docSize = 0;
75822
+ /**
75823
+ * Build the search index from a ProseMirror document.
75824
+ * Uses doc.textBetween for the flattened string and walks
75825
+ * the document to build the segment offset map.
75826
+ *
75827
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
75828
+ */
75829
+ build(doc2) {
75830
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
75831
+ this.segments = [];
75832
+ this.docSize = doc2.content.size;
75833
+ let offset2 = 0;
75834
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
75835
+ this.segments.push(segment);
75836
+ offset2 = segment.offsetEnd;
75837
+ });
75838
+ this.valid = true;
75839
+ }
75840
+ /**
75841
+ * Walk the content of a node to build segments.
75842
+ * This method processes the children of a node, given the position
75843
+ * where the node's content starts.
75844
+ *
75845
+ * @param {import('prosemirror-model').Node} node - Current node
75846
+ * @param {number} contentStart - Document position where this node's content starts
75847
+ * @param {number} offset - Current offset in flattened string
75848
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75849
+ * @returns {number} The new offset after processing this node's content
75850
+ */
75851
+ #walkNodeContent(node, contentStart, offset2, addSegment) {
75852
+ let currentOffset = offset2;
75853
+ let isFirstChild = true;
75854
+ node.forEach((child, childContentOffset) => {
75855
+ const childDocPos = contentStart + childContentOffset;
75856
+ if (child.isBlock && !isFirstChild) {
75857
+ addSegment({
75858
+ offsetStart: currentOffset,
75859
+ offsetEnd: currentOffset + 1,
75860
+ docFrom: childDocPos,
75861
+ docTo: childDocPos,
75862
+ kind: "blockSep"
75863
+ });
75864
+ currentOffset += 1;
75865
+ }
75866
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
75867
+ isFirstChild = false;
75868
+ });
75869
+ return currentOffset;
75870
+ }
75871
+ /**
75872
+ * Recursively walk a node and its descendants to build segments.
75873
+ *
75874
+ * @param {import('prosemirror-model').Node} node - Current node
75875
+ * @param {number} docPos - Document position at start of this node
75876
+ * @param {number} offset - Current offset in flattened string
75877
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75878
+ * @returns {number} The new offset after processing this node
75879
+ */
75880
+ #walkNode(node, docPos, offset2, addSegment) {
75881
+ if (node.isText) {
75882
+ const text = node.text || "";
75883
+ if (text.length > 0) {
75884
+ addSegment({
75885
+ offsetStart: offset2,
75886
+ offsetEnd: offset2 + text.length,
75887
+ docFrom: docPos,
75888
+ docTo: docPos + text.length,
75889
+ kind: "text"
75890
+ });
75891
+ return offset2 + text.length;
75892
+ }
75893
+ return offset2;
75894
+ }
75895
+ if (node.isLeaf) {
75896
+ if (node.type.name === "hard_break") {
75897
+ addSegment({
75898
+ offsetStart: offset2,
75899
+ offsetEnd: offset2 + 1,
75900
+ docFrom: docPos,
75901
+ docTo: docPos + node.nodeSize,
75902
+ kind: "hardBreak"
75903
+ });
75904
+ return offset2 + 1;
75905
+ }
75906
+ addSegment({
75907
+ offsetStart: offset2,
75908
+ offsetEnd: offset2 + 1,
75909
+ docFrom: docPos,
75910
+ docTo: docPos + node.nodeSize,
75911
+ kind: "atom"
75912
+ });
75913
+ return offset2 + 1;
75914
+ }
75915
+ return this.#walkNodeContent(node, docPos + 1, offset2, addSegment);
75916
+ }
75917
+ /**
75918
+ * Mark the index as stale. It will be rebuilt on next search.
75919
+ */
75920
+ invalidate() {
75921
+ this.valid = false;
75922
+ }
75923
+ /**
75924
+ * Check if the index needs rebuilding for the given document.
75925
+ *
75926
+ * @param {import('prosemirror-model').Node} doc - The document to check against
75927
+ * @returns {boolean} True if index is stale and needs rebuilding
75928
+ */
75929
+ isStale(doc2) {
75930
+ return !this.valid || doc2.content.size !== this.docSize;
75931
+ }
75932
+ /**
75933
+ * Ensure the index is valid for the given document.
75934
+ * Rebuilds if stale.
75935
+ *
75936
+ * @param {import('prosemirror-model').Node} doc - The document
75937
+ */
75938
+ ensureValid(doc2) {
75939
+ if (this.isStale(doc2)) {
75940
+ this.build(doc2);
75941
+ }
75942
+ }
75943
+ /**
75944
+ * Convert an offset range in the flattened string to document ranges.
75945
+ * Skips separator/atom segments and returns only text ranges.
75946
+ *
75947
+ * @param {number} start - Start offset in flattened string
75948
+ * @param {number} end - End offset in flattened string
75949
+ * @returns {DocRange[]} Array of document ranges (text segments only)
75950
+ */
75951
+ offsetRangeToDocRanges(start2, end2) {
75952
+ const ranges = [];
75953
+ for (const segment of this.segments) {
75954
+ if (segment.offsetEnd <= start2) continue;
75955
+ if (segment.offsetStart >= end2) break;
75956
+ if (segment.kind !== "text") continue;
75957
+ const overlapStart = Math.max(start2, segment.offsetStart);
75958
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
75959
+ if (overlapStart < overlapEnd) {
75960
+ const startInSegment = overlapStart - segment.offsetStart;
75961
+ const endInSegment = overlapEnd - segment.offsetStart;
75962
+ ranges.push({
75963
+ from: segment.docFrom + startInSegment,
75964
+ to: segment.docFrom + endInSegment
75965
+ });
75966
+ }
75967
+ }
75968
+ return ranges;
75969
+ }
75970
+ /**
75971
+ * Find the document position for a given offset in the flattened string.
75972
+ *
75973
+ * @param {number} offset - Offset in flattened string
75974
+ * @returns {number|null} Document position, or null if not found
75975
+ */
75976
+ offsetToDocPos(offset2) {
75977
+ for (const segment of this.segments) {
75978
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
75979
+ if (segment.kind === "text") {
75980
+ return segment.docFrom + (offset2 - segment.offsetStart);
75981
+ }
75982
+ return segment.docFrom;
75983
+ }
75984
+ }
75985
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
75986
+ const lastSeg = this.segments[this.segments.length - 1];
75987
+ return lastSeg.docTo;
75988
+ }
75989
+ return null;
75990
+ }
75991
+ /**
75992
+ * Escape special regex characters in a string.
75993
+ *
75994
+ * @param {string} str - String to escape
75995
+ * @returns {string} Escaped string safe for use in RegExp
75996
+ */
75997
+ static escapeRegex(str) {
75998
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75999
+ }
76000
+ /**
76001
+ * Convert a plain search string to a whitespace-flexible regex pattern.
76002
+ * This allows matching across paragraph boundaries.
76003
+ *
76004
+ * @param {string} searchString - The search string
76005
+ * @returns {string} Regex pattern string
76006
+ */
76007
+ static toFlexiblePattern(searchString) {
76008
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
76009
+ if (parts.length === 0) return "";
76010
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
76011
+ }
76012
+ /**
76013
+ * Search the index for matches.
76014
+ *
76015
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
76016
+ * @param {Object} options - Search options
76017
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
76018
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
76019
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
76020
+ */
76021
+ search(pattern, options = {}) {
76022
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
76023
+ const matches = [];
76024
+ let regex;
76025
+ if (pattern instanceof RegExp) {
76026
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
76027
+ regex = new RegExp(pattern.source, flags);
76028
+ } else if (typeof pattern === "string") {
76029
+ if (pattern.length === 0) return matches;
76030
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
76031
+ if (flexiblePattern.length === 0) return matches;
76032
+ const flags = caseSensitive ? "g" : "gi";
76033
+ regex = new RegExp(flexiblePattern, flags);
76034
+ } else {
76035
+ return matches;
76036
+ }
76037
+ let match;
76038
+ while ((match = regex.exec(this.text)) !== null && matches.length < maxMatches) {
76039
+ matches.push({
76040
+ start: match.index,
76041
+ end: match.index + match[0].length,
76042
+ text: match[0]
76043
+ });
76044
+ if (match[0].length === 0) {
76045
+ regex.lastIndex++;
76046
+ }
76047
+ }
76048
+ return matches;
75550
76049
  }
75551
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75552
- return tr.setMeta(searchKey, { query, range, highlight });
75553
76050
  }
76051
+ const customSearchHighlightsKey = new superEditor_converter.PluginKey("customSearchHighlights");
75554
76052
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
76053
+ const resolveInlineTextPosition = (doc2, position, direction) => {
76054
+ const docSize = doc2.content.size;
76055
+ if (!Number.isFinite(position) || position < 0 || position > docSize) {
76056
+ return position;
76057
+ }
76058
+ const step = direction === "forward" ? 1 : -1;
76059
+ let current = position;
76060
+ let iterations = 0;
76061
+ while (iterations < 8) {
76062
+ iterations += 1;
76063
+ const resolved = doc2.resolve(current);
76064
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
76065
+ if (!boundaryNode) break;
76066
+ if (boundaryNode.isText) break;
76067
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
76068
+ const next = current + step;
76069
+ if (next < 0 || next > docSize) break;
76070
+ current = next;
76071
+ const adjacent = doc2.resolve(current);
76072
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
76073
+ if (checkNode && checkNode.isText) break;
76074
+ }
76075
+ return current;
76076
+ };
76077
+ const resolveSearchRange = ({ doc: doc2, from: from3, to, expectedText, highlights }) => {
76078
+ const docSize = doc2.content.size;
76079
+ let resolvedFrom = Math.max(0, Math.min(from3, docSize));
76080
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
76081
+ if (highlights) {
76082
+ const windowStart = Math.max(0, resolvedFrom - 4);
76083
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
76084
+ const candidates = highlights.find(windowStart, windowEnd);
76085
+ if (candidates.length > 0) {
76086
+ let chosen = candidates[0];
76087
+ if (expectedText) {
76088
+ const matching = candidates.filter(
76089
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
76090
+ );
76091
+ if (matching.length > 0) {
76092
+ chosen = matching[0];
76093
+ }
76094
+ }
76095
+ resolvedFrom = chosen.from;
76096
+ resolvedTo = chosen.to;
76097
+ }
76098
+ }
76099
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
76100
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
76101
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
76102
+ resolvedFrom = normalizedFrom;
76103
+ resolvedTo = normalizedTo;
76104
+ }
76105
+ return { from: resolvedFrom, to: resolvedTo };
76106
+ };
76107
+ const getPositionTracker = (editor) => {
76108
+ if (!editor) return null;
76109
+ if (editor.positionTracker) return editor.positionTracker;
76110
+ const storageTracker = editor.storage?.positionTracker?.tracker;
76111
+ if (storageTracker) {
76112
+ editor.positionTracker = storageTracker;
76113
+ return storageTracker;
76114
+ }
76115
+ const tracker = new PositionTracker(editor);
76116
+ if (editor.storage?.positionTracker) {
76117
+ editor.storage.positionTracker.tracker = tracker;
76118
+ }
76119
+ editor.positionTracker = tracker;
76120
+ return tracker;
76121
+ };
75555
76122
  const Search = Extension.create({
75556
76123
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
75557
76124
  addStorage() {
@@ -75560,29 +76127,58 @@ const Search = Extension.create({
75560
76127
  * @private
75561
76128
  * @type {SearchMatch[]|null}
75562
76129
  */
75563
- searchResults: []
76130
+ searchResults: [],
76131
+ /**
76132
+ * @private
76133
+ * @type {boolean}
76134
+ * Whether to apply CSS highlight classes to matches
76135
+ */
76136
+ highlightEnabled: true,
76137
+ /**
76138
+ * @private
76139
+ * @type {SearchIndex}
76140
+ * Lazily-built search index for cross-paragraph matching
76141
+ */
76142
+ searchIndex: new SearchIndex()
75564
76143
  };
75565
76144
  },
75566
76145
  addPmPlugins() {
75567
76146
  const editor = this.editor;
75568
76147
  const storage = this.storage;
76148
+ const searchIndexInvalidatorPlugin = new superEditor_converter.Plugin({
76149
+ key: new superEditor_converter.PluginKey("searchIndexInvalidator"),
76150
+ appendTransaction(transactions, oldState, newState) {
76151
+ const docChanged = transactions.some((tr) => tr.docChanged);
76152
+ if (docChanged && storage?.searchIndex) {
76153
+ storage.searchIndex.invalidate();
76154
+ }
76155
+ return null;
76156
+ }
76157
+ });
75569
76158
  const searchHighlightWithIdPlugin = new superEditor_converter.Plugin({
75570
- key: new superEditor_converter.PluginKey("customSearchHighlights"),
76159
+ key: customSearchHighlightsKey,
75571
76160
  props: {
75572
76161
  decorations(state) {
75573
76162
  if (!editor) return null;
75574
76163
  const matches = storage?.searchResults;
75575
76164
  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
- );
76165
+ const highlightEnabled = storage?.highlightEnabled !== false;
76166
+ const decorations = [];
76167
+ for (const match of matches) {
76168
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
76169
+ if (match.ranges && match.ranges.length > 0) {
76170
+ for (const range of match.ranges) {
76171
+ decorations.push(Decoration.inline(range.from, range.to, attrs));
76172
+ }
76173
+ } else {
76174
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
76175
+ }
76176
+ }
75581
76177
  return DecorationSet.create(state.doc, decorations);
75582
76178
  }
75583
76179
  }
75584
76180
  });
75585
- return [search(), searchHighlightWithIdPlugin];
76181
+ return [search(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
75586
76182
  },
75587
76183
  addCommands() {
75588
76184
  return {
@@ -75596,21 +76192,51 @@ const Search = Extension.create({
75596
76192
  goToFirstMatch: () => (
75597
76193
  /** @returns {boolean} */
75598
76194
  ({ state, editor, dispatch }) => {
76195
+ const searchResults = this.storage?.searchResults;
76196
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
76197
+ const firstMatch = searchResults[0];
76198
+ const from3 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
76199
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
76200
+ if (typeof from3 !== "number" || typeof to !== "number") {
76201
+ return false;
76202
+ }
76203
+ editor.view.focus();
76204
+ const tr2 = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, from3, to)).scrollIntoView();
76205
+ if (dispatch) dispatch(tr2);
76206
+ const presentationEditor2 = editor.presentationEditor;
76207
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
76208
+ const didScroll = presentationEditor2.scrollToPosition(from3, { block: "center" });
76209
+ if (didScroll) return true;
76210
+ }
76211
+ try {
76212
+ const domPos = editor.view.domAtPos(from3);
76213
+ if (domPos?.node?.scrollIntoView) {
76214
+ domPos.node.scrollIntoView(true);
76215
+ }
76216
+ } catch {
76217
+ }
76218
+ return true;
76219
+ }
75599
76220
  const highlights = getMatchHighlights(state);
75600
76221
  if (!highlights) return false;
75601
76222
  const decorations = highlights.find();
75602
76223
  if (!decorations?.length) return false;
75603
- const firstMatch = decorations[0];
76224
+ const firstDeco = decorations[0];
75604
76225
  editor.view.focus();
75605
- const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
76226
+ const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
75606
76227
  if (dispatch) dispatch(tr);
75607
76228
  const presentationEditor = editor.presentationEditor;
75608
76229
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
75609
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
76230
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
75610
76231
  if (didScroll) return true;
75611
76232
  }
75612
- const domPos = editor.view.domAtPos(firstMatch.from);
75613
- domPos?.node?.scrollIntoView(true);
76233
+ try {
76234
+ const domPos = editor.view.domAtPos(firstDeco.from);
76235
+ if (domPos?.node?.scrollIntoView) {
76236
+ domPos.node.scrollIntoView(true);
76237
+ }
76238
+ } catch {
76239
+ }
75614
76240
  return true;
75615
76241
  }
75616
76242
  ),
@@ -75628,53 +76254,57 @@ const Search = Extension.create({
75628
76254
  *
75629
76255
  * // Search without visual highlighting
75630
76256
  * const silentMatches = editor.commands.search('test', { highlight: false })
75631
- * @note Returns array of SearchMatch objects with positions and IDs
76257
+ *
76258
+ * // Cross-paragraph search (works by default for plain strings)
76259
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
76260
+ * @note Returns array of SearchMatch objects with positions and IDs.
76261
+ * Plain string searches are whitespace-flexible and match across paragraphs.
76262
+ * Regex searches match exactly as specified.
75632
76263
  */
75633
76264
  search: (patternInput, options = {}) => (
75634
76265
  /** @returns {SearchMatch[]} */
75635
- ({ state, dispatch }) => {
76266
+ ({ state, dispatch, editor }) => {
75636
76267
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75637
76268
  throw new TypeError("Search options must be an object");
75638
76269
  }
75639
76270
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75640
- let pattern;
76271
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
75641
76272
  let caseSensitive = false;
75642
- let regexp = false;
75643
- const wholeWord = false;
76273
+ let searchPattern = patternInput;
75644
76274
  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");
76275
+ caseSensitive = !patternInput.flags.includes("i");
76276
+ searchPattern = patternInput;
75652
76277
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
75653
76278
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
75654
- regexp = true;
75655
- pattern = body;
75656
76279
  caseSensitive = !flags.includes("i");
76280
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
75657
76281
  } else {
75658
- pattern = String(patternInput);
76282
+ searchPattern = String(patternInput);
75659
76283
  }
75660
- const query = new SearchQuery({
75661
- search: pattern,
76284
+ const searchIndex = this.storage.searchIndex;
76285
+ searchIndex.ensureValid(state.doc);
76286
+ const indexMatches = searchIndex.search(searchPattern, {
75662
76287
  caseSensitive,
75663
- regexp,
75664
- wholeWord
76288
+ maxMatches
75665
76289
  });
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
- }));
76290
+ const resultMatches = [];
76291
+ for (const indexMatch of indexMatches) {
76292
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
76293
+ if (ranges.length === 0) continue;
76294
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
76295
+ const combinedText = matchTexts.join("");
76296
+ const match = {
76297
+ from: ranges[0].from,
76298
+ to: ranges[ranges.length - 1].to,
76299
+ text: combinedText,
76300
+ id: uuid.v4(),
76301
+ ranges,
76302
+ trackerIds: []
76303
+ };
76304
+ resultMatches.push(match);
76305
+ }
75677
76306
  this.storage.searchResults = resultMatches;
76307
+ this.storage.highlightEnabled = highlight;
75678
76308
  return resultMatches;
75679
76309
  }
75680
76310
  ),
@@ -75685,12 +76315,48 @@ const Search = Extension.create({
75685
76315
  * @example
75686
76316
  * const searchResults = editor.commands.search('test string')
75687
76317
  * editor.commands.goToSearchResult(searchResults[3])
75688
- * @note Scrolls to match and selects it
76318
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
76319
+ * selects the first range and scrolls to it.
75689
76320
  */
75690
76321
  goToSearchResult: (match) => (
75691
76322
  /** @returns {boolean} */
75692
76323
  ({ state, dispatch, editor }) => {
75693
- const { from: from3, to } = match;
76324
+ const positionTracker = getPositionTracker(editor);
76325
+ const doc2 = state.doc;
76326
+ const highlights = getMatchHighlights(state);
76327
+ let from3, to;
76328
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
76329
+ if (positionTracker?.resolve && match.trackerIds[0]) {
76330
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
76331
+ if (resolved) {
76332
+ from3 = resolved.from;
76333
+ to = resolved.to;
76334
+ }
76335
+ }
76336
+ if (from3 === void 0) {
76337
+ from3 = match.ranges[0].from;
76338
+ to = match.ranges[0].to;
76339
+ }
76340
+ } else {
76341
+ from3 = match.from;
76342
+ to = match.to;
76343
+ if (positionTracker?.resolve && match?.id) {
76344
+ const resolved = positionTracker.resolve(match.id);
76345
+ if (resolved) {
76346
+ from3 = resolved.from;
76347
+ to = resolved.to;
76348
+ }
76349
+ }
76350
+ }
76351
+ const normalized = resolveSearchRange({
76352
+ doc: doc2,
76353
+ from: from3,
76354
+ to,
76355
+ expectedText: match?.text ?? null,
76356
+ highlights
76357
+ });
76358
+ from3 = normalized.from;
76359
+ to = normalized.to;
75694
76360
  editor.view.focus();
75695
76361
  const tr = state.tr.setSelection(superEditor_converter.TextSelection.create(state.doc, from3, to)).scrollIntoView();
75696
76362
  if (dispatch) dispatch(tr);