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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { B as Buffer$2 } from "./jszip-B1fkPkPJ.es.js";
2
2
  import { t as twipsToInches, i as inchesToTwips, p as ptToTwips, l as linesToTwips, a as twipsToLines, b as pixelsToTwips, h as halfPointToPoints, c as twipsToPixels$2, d as convertSizeToCSS, e as inchesToPixels } from "./helpers-C8e9wR5l.es.js";
3
- import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as resolveRunProperties, u as encodeMarksFromRPr, v as liftTarget, x as canJoin, y as joinPoint, z as replaceStep$1, R as ReplaceAroundStep$1, B as htmlHandler, C as ReplaceStep, E as getResolvedParagraphProperties, G as changeListLevel, H as isList$1, I as updateNumberingProperties, L as ListHelpers, J as inputRulesPlugin, K as TrackDeleteMarkName$1, O as TrackInsertMarkName$1, Q as TrackFormatMarkName$1, U as AddMarkStep, V as RemoveMarkStep, W as CommandService, S as SuperConverter, X as EditorState, Y as unflattenListsInHtml, Z as SelectionRange, _ as Transform, $ as createOoxmlResolver, a0 as translator, a1 as translator$1, a2 as resolveDocxFontFamily, a3 as combineIndentProperties, a4 as _getReferencedTableStyles, a5 as decodeRPrFromMarks, a6 as calculateResolvedParagraphProperties, a7 as encodeCSSFromPPr, a8 as encodeCSSFromRPr, a9 as generateOrderedListIndex, aa as docxNumberingHelpers, ab as InputRule, ac as insertNewRelationship, ad as kebabCase$1, ae as getUnderlineCssString } from "./SuperConverter-Cyn9peRO.es.js";
3
+ import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as resolveRunProperties, u as encodeMarksFromRPr, v as liftTarget, x as canJoin, y as joinPoint, z as replaceStep$1, R as ReplaceAroundStep$1, B as htmlHandler, C as ReplaceStep, E as getResolvedParagraphProperties, G as changeListLevel, H as isList$1, I as updateNumberingProperties, L as ListHelpers, J as inputRulesPlugin, K as TrackDeleteMarkName$1, O as TrackInsertMarkName$1, Q as TrackFormatMarkName$1, U as AddMarkStep, V as RemoveMarkStep, W as CommandService, S as SuperConverter, X as EditorState, Y as unflattenListsInHtml, Z as SelectionRange, _ as Transform, $ as createOoxmlResolver, a0 as translator, a1 as translator$1, a2 as resolveDocxFontFamily, a3 as combineIndentProperties, a4 as _getReferencedTableStyles, a5 as decodeRPrFromMarks, a6 as calculateResolvedParagraphProperties, a7 as encodeCSSFromPPr, a8 as encodeCSSFromRPr, a9 as generateOrderedListIndex, aa as docxNumberingHelpers, ab as InputRule, ac as insertNewRelationship, ad as kebabCase$1, ae as getUnderlineCssString } from "./SuperConverter-BAUfsE-s.es.js";
4
4
  import { p as process$1, r as ref, C as global$1, c as computed, E as createElementBlock, F as Fragment$1, S as renderList, O as withModifiers, G as openBlock, P as normalizeClass, M as createCommentVNode, H as toDisplayString, K as createBaseVNode, U as createApp, f as onMounted, X as onUnmounted, R as withDirectives, v as unref, Y as vModelText, y as nextTick, L as normalizeStyle, u as watch, Z as withKeys, _ as createTextVNode, I as createVNode, h as h$1, $ as readonly, s as getCurrentInstance, o as onBeforeUnmount, j as reactive, b as onBeforeMount, i as inject, a0 as onActivated, a1 as onDeactivated, a2 as Comment, d as defineComponent, a as provide, g as Teleport, t as toRef, a3 as renderSlot, a4 as isVNode, D as shallowRef, w as watchEffect, T as Transition, a5 as mergeProps, a6 as vShow, a7 as cloneVNode, a8 as Text$2, m as markRaw, N as createBlock, J as withCtx, a9 as useCssVars, V as resolveDynamicComponent, aa as normalizeProps, ab as guardReactiveProps } from "./vue-BnBKJwCW.es.js";
5
5
  import "./jszip.min-DCl8qkFO.es.js";
6
6
  import { E as EventEmitter$1 } from "./eventemitter3-CwrdEv8r.es.js";
@@ -9370,6 +9370,158 @@ class Schema {
9370
9370
  return Object.fromEntries(markEntries);
9371
9371
  }
9372
9372
  }
9373
+ const positionTrackerKey = new PluginKey("positionTracker");
9374
+ function createPositionTrackerPlugin() {
9375
+ return new Plugin({
9376
+ key: positionTrackerKey,
9377
+ state: {
9378
+ init() {
9379
+ return {
9380
+ decorations: DecorationSet.empty,
9381
+ generation: 0
9382
+ };
9383
+ },
9384
+ apply(tr, state) {
9385
+ let { decorations, generation } = state;
9386
+ const meta = tr.getMeta(positionTrackerKey);
9387
+ if (meta?.action === "add") {
9388
+ decorations = decorations.add(tr.doc, meta.decorations);
9389
+ } else if (meta?.action === "remove") {
9390
+ const toRemove = decorations.find().filter((decoration) => meta.ids.includes(decoration.spec.id));
9391
+ decorations = decorations.remove(toRemove);
9392
+ } else if (meta?.action === "removeByType") {
9393
+ const toRemove = decorations.find().filter((decoration) => decoration.spec.type === meta.type);
9394
+ decorations = decorations.remove(toRemove);
9395
+ }
9396
+ if (tr.docChanged) {
9397
+ decorations = decorations.map(tr.mapping, tr.doc);
9398
+ generation += 1;
9399
+ }
9400
+ return { decorations, generation };
9401
+ }
9402
+ },
9403
+ props: {
9404
+ decorations() {
9405
+ return DecorationSet.empty;
9406
+ }
9407
+ }
9408
+ });
9409
+ }
9410
+ class PositionTracker {
9411
+ #editor;
9412
+ constructor(editor) {
9413
+ this.#editor = editor;
9414
+ }
9415
+ #getState() {
9416
+ if (!this.#editor?.state) return null;
9417
+ return positionTrackerKey.getState(this.#editor.state) ?? null;
9418
+ }
9419
+ track(from3, to, spec) {
9420
+ const id = v4();
9421
+ if (!this.#editor?.state) return id;
9422
+ const fullSpec = { kind: "range", ...spec, id };
9423
+ const deco = Decoration.inline(from3, to, {}, fullSpec);
9424
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9425
+ action: "add",
9426
+ decorations: [deco]
9427
+ }).setMeta("addToHistory", false);
9428
+ this.#editor.dispatch(tr);
9429
+ return id;
9430
+ }
9431
+ trackMany(ranges) {
9432
+ if (!this.#editor?.state) {
9433
+ return ranges.map(() => v4());
9434
+ }
9435
+ const ids = [];
9436
+ const decorations = [];
9437
+ for (const { from: from3, to, spec } of ranges) {
9438
+ const id = v4();
9439
+ ids.push(id);
9440
+ const fullSpec = { kind: "range", ...spec, id };
9441
+ decorations.push(Decoration.inline(from3, to, {}, fullSpec));
9442
+ }
9443
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9444
+ action: "add",
9445
+ decorations
9446
+ }).setMeta("addToHistory", false);
9447
+ this.#editor.dispatch(tr);
9448
+ return ids;
9449
+ }
9450
+ untrack(id) {
9451
+ if (!this.#editor?.state) return;
9452
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9453
+ action: "remove",
9454
+ ids: [id]
9455
+ }).setMeta("addToHistory", false);
9456
+ this.#editor.dispatch(tr);
9457
+ }
9458
+ untrackMany(ids) {
9459
+ if (!this.#editor?.state || ids.length === 0) return;
9460
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9461
+ action: "remove",
9462
+ ids
9463
+ }).setMeta("addToHistory", false);
9464
+ this.#editor.dispatch(tr);
9465
+ }
9466
+ untrackByType(type) {
9467
+ if (!this.#editor?.state) return;
9468
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9469
+ action: "removeByType",
9470
+ type
9471
+ }).setMeta("addToHistory", false);
9472
+ this.#editor.dispatch(tr);
9473
+ }
9474
+ resolve(id) {
9475
+ const state = this.#getState();
9476
+ if (!state) return null;
9477
+ const found = state.decorations.find().find((decoration) => decoration.spec.id === id);
9478
+ if (!found) return null;
9479
+ const spec = found.spec;
9480
+ return {
9481
+ id: spec.id,
9482
+ from: found.from,
9483
+ to: found.to,
9484
+ spec
9485
+ };
9486
+ }
9487
+ resolveMany(ids) {
9488
+ const result = /* @__PURE__ */ new Map();
9489
+ for (const id of ids) {
9490
+ result.set(id, null);
9491
+ }
9492
+ const state = this.#getState();
9493
+ if (!state || ids.length === 0) return result;
9494
+ const idSet = new Set(ids);
9495
+ for (const decoration of state.decorations.find()) {
9496
+ const spec = decoration.spec;
9497
+ if (idSet.has(spec.id)) {
9498
+ result.set(spec.id, {
9499
+ id: spec.id,
9500
+ from: decoration.from,
9501
+ to: decoration.to,
9502
+ spec
9503
+ });
9504
+ }
9505
+ }
9506
+ return result;
9507
+ }
9508
+ findByType(type) {
9509
+ const state = this.#getState();
9510
+ if (!state) return [];
9511
+ return state.decorations.find().filter((decoration) => decoration.spec.type === type).map((decoration) => {
9512
+ const spec = decoration.spec;
9513
+ return {
9514
+ id: spec.id,
9515
+ from: decoration.from,
9516
+ to: decoration.to,
9517
+ spec
9518
+ };
9519
+ });
9520
+ }
9521
+ get generation() {
9522
+ return this.#getState()?.generation ?? 0;
9523
+ }
9524
+ }
9373
9525
  class OxmlNode extends Node$1 {
9374
9526
  constructor(config) {
9375
9527
  super(config);
@@ -11730,6 +11882,34 @@ const EditorFocus = Extension.create({
11730
11882
  return [editorFocusPlugin];
11731
11883
  }
11732
11884
  });
11885
+ const PositionTrackerExtension = Extension.create({
11886
+ name: "positionTracker",
11887
+ addStorage() {
11888
+ return {
11889
+ tracker: null
11890
+ };
11891
+ },
11892
+ addPmPlugins() {
11893
+ return [createPositionTrackerPlugin()];
11894
+ },
11895
+ onCreate() {
11896
+ const existing = this.editor?.positionTracker ?? this.storage.tracker;
11897
+ if (existing) {
11898
+ this.storage.tracker = existing;
11899
+ this.editor.positionTracker = existing;
11900
+ return;
11901
+ }
11902
+ const tracker = new PositionTracker(this.editor);
11903
+ this.storage.tracker = tracker;
11904
+ this.editor.positionTracker = tracker;
11905
+ },
11906
+ onDestroy() {
11907
+ if (this.editor?.positionTracker === this.storage.tracker) {
11908
+ this.editor.positionTracker = null;
11909
+ }
11910
+ this.storage.tracker = null;
11911
+ }
11912
+ });
11733
11913
  class EventEmitter {
11734
11914
  #events = /* @__PURE__ */ new Map();
11735
11915
  /**
@@ -15507,7 +15687,7 @@ const canUseDOM = () => {
15507
15687
  return false;
15508
15688
  }
15509
15689
  };
15510
- const summaryVersion = "1.6.0-next.1";
15690
+ const summaryVersion = "1.6.0-next.2";
15511
15691
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
15512
15692
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
15513
15693
  function mapAttributes(attrs) {
@@ -17000,7 +17180,7 @@ class Editor extends EventEmitter {
17000
17180
  */
17001
17181
  #createExtensionService() {
17002
17182
  const allowedExtensions = ["extension", "node", "mark"];
17003
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
17183
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
17004
17184
  const externalExtensions = this.options.externalExtensions || [];
17005
17185
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
17006
17186
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -18174,7 +18354,7 @@ class Editor extends EventEmitter {
18174
18354
  * Process collaboration migrations
18175
18355
  */
18176
18356
  processCollaborationMigrations() {
18177
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
18357
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.2");
18178
18358
  if (!this.options.ydoc) return;
18179
18359
  const metaMap = this.options.ydoc.getMap("meta");
18180
18360
  let docVersion = metaMap.get("version");
@@ -75527,14 +75707,317 @@ function getMatchHighlights(state) {
75527
75707
  let search2 = searchKey.getState(state);
75528
75708
  return search2 ? search2.deco : DecorationSet.empty;
75529
75709
  }
75530
- function setSearchState(tr, query, range = null, options = {}) {
75531
- if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75532
- throw new TypeError("setSearchState options must be an object");
75710
+ const BLOCK_SEPARATOR = "\n";
75711
+ const ATOM_PLACEHOLDER = "";
75712
+ class SearchIndex {
75713
+ /** @type {string} */
75714
+ text = "";
75715
+ /** @type {Segment[]} */
75716
+ segments = [];
75717
+ /** @type {boolean} */
75718
+ valid = false;
75719
+ /** @type {number} */
75720
+ docSize = 0;
75721
+ /**
75722
+ * Build the search index from a ProseMirror document.
75723
+ * Uses doc.textBetween for the flattened string and walks
75724
+ * the document to build the segment offset map.
75725
+ *
75726
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
75727
+ */
75728
+ build(doc2) {
75729
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
75730
+ this.segments = [];
75731
+ this.docSize = doc2.content.size;
75732
+ let offset2 = 0;
75733
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
75734
+ this.segments.push(segment);
75735
+ offset2 = segment.offsetEnd;
75736
+ });
75737
+ this.valid = true;
75738
+ }
75739
+ /**
75740
+ * Walk the content of a node to build segments.
75741
+ * This method processes the children of a node, given the position
75742
+ * where the node's content starts.
75743
+ *
75744
+ * @param {import('prosemirror-model').Node} node - Current node
75745
+ * @param {number} contentStart - Document position where this node's content starts
75746
+ * @param {number} offset - Current offset in flattened string
75747
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75748
+ * @returns {number} The new offset after processing this node's content
75749
+ */
75750
+ #walkNodeContent(node, contentStart, offset2, addSegment) {
75751
+ let currentOffset = offset2;
75752
+ let isFirstChild = true;
75753
+ node.forEach((child, childContentOffset) => {
75754
+ const childDocPos = contentStart + childContentOffset;
75755
+ if (child.isBlock && !isFirstChild) {
75756
+ addSegment({
75757
+ offsetStart: currentOffset,
75758
+ offsetEnd: currentOffset + 1,
75759
+ docFrom: childDocPos,
75760
+ docTo: childDocPos,
75761
+ kind: "blockSep"
75762
+ });
75763
+ currentOffset += 1;
75764
+ }
75765
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
75766
+ isFirstChild = false;
75767
+ });
75768
+ return currentOffset;
75769
+ }
75770
+ /**
75771
+ * Recursively walk a node and its descendants to build segments.
75772
+ *
75773
+ * @param {import('prosemirror-model').Node} node - Current node
75774
+ * @param {number} docPos - Document position at start of this node
75775
+ * @param {number} offset - Current offset in flattened string
75776
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75777
+ * @returns {number} The new offset after processing this node
75778
+ */
75779
+ #walkNode(node, docPos, offset2, addSegment) {
75780
+ if (node.isText) {
75781
+ const text = node.text || "";
75782
+ if (text.length > 0) {
75783
+ addSegment({
75784
+ offsetStart: offset2,
75785
+ offsetEnd: offset2 + text.length,
75786
+ docFrom: docPos,
75787
+ docTo: docPos + text.length,
75788
+ kind: "text"
75789
+ });
75790
+ return offset2 + text.length;
75791
+ }
75792
+ return offset2;
75793
+ }
75794
+ if (node.isLeaf) {
75795
+ if (node.type.name === "hard_break") {
75796
+ addSegment({
75797
+ offsetStart: offset2,
75798
+ offsetEnd: offset2 + 1,
75799
+ docFrom: docPos,
75800
+ docTo: docPos + node.nodeSize,
75801
+ kind: "hardBreak"
75802
+ });
75803
+ return offset2 + 1;
75804
+ }
75805
+ addSegment({
75806
+ offsetStart: offset2,
75807
+ offsetEnd: offset2 + 1,
75808
+ docFrom: docPos,
75809
+ docTo: docPos + node.nodeSize,
75810
+ kind: "atom"
75811
+ });
75812
+ return offset2 + 1;
75813
+ }
75814
+ return this.#walkNodeContent(node, docPos + 1, offset2, addSegment);
75815
+ }
75816
+ /**
75817
+ * Mark the index as stale. It will be rebuilt on next search.
75818
+ */
75819
+ invalidate() {
75820
+ this.valid = false;
75821
+ }
75822
+ /**
75823
+ * Check if the index needs rebuilding for the given document.
75824
+ *
75825
+ * @param {import('prosemirror-model').Node} doc - The document to check against
75826
+ * @returns {boolean} True if index is stale and needs rebuilding
75827
+ */
75828
+ isStale(doc2) {
75829
+ return !this.valid || doc2.content.size !== this.docSize;
75830
+ }
75831
+ /**
75832
+ * Ensure the index is valid for the given document.
75833
+ * Rebuilds if stale.
75834
+ *
75835
+ * @param {import('prosemirror-model').Node} doc - The document
75836
+ */
75837
+ ensureValid(doc2) {
75838
+ if (this.isStale(doc2)) {
75839
+ this.build(doc2);
75840
+ }
75841
+ }
75842
+ /**
75843
+ * Convert an offset range in the flattened string to document ranges.
75844
+ * Skips separator/atom segments and returns only text ranges.
75845
+ *
75846
+ * @param {number} start - Start offset in flattened string
75847
+ * @param {number} end - End offset in flattened string
75848
+ * @returns {DocRange[]} Array of document ranges (text segments only)
75849
+ */
75850
+ offsetRangeToDocRanges(start2, end2) {
75851
+ const ranges = [];
75852
+ for (const segment of this.segments) {
75853
+ if (segment.offsetEnd <= start2) continue;
75854
+ if (segment.offsetStart >= end2) break;
75855
+ if (segment.kind !== "text") continue;
75856
+ const overlapStart = Math.max(start2, segment.offsetStart);
75857
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
75858
+ if (overlapStart < overlapEnd) {
75859
+ const startInSegment = overlapStart - segment.offsetStart;
75860
+ const endInSegment = overlapEnd - segment.offsetStart;
75861
+ ranges.push({
75862
+ from: segment.docFrom + startInSegment,
75863
+ to: segment.docFrom + endInSegment
75864
+ });
75865
+ }
75866
+ }
75867
+ return ranges;
75868
+ }
75869
+ /**
75870
+ * Find the document position for a given offset in the flattened string.
75871
+ *
75872
+ * @param {number} offset - Offset in flattened string
75873
+ * @returns {number|null} Document position, or null if not found
75874
+ */
75875
+ offsetToDocPos(offset2) {
75876
+ for (const segment of this.segments) {
75877
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
75878
+ if (segment.kind === "text") {
75879
+ return segment.docFrom + (offset2 - segment.offsetStart);
75880
+ }
75881
+ return segment.docFrom;
75882
+ }
75883
+ }
75884
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
75885
+ const lastSeg = this.segments[this.segments.length - 1];
75886
+ return lastSeg.docTo;
75887
+ }
75888
+ return null;
75889
+ }
75890
+ /**
75891
+ * Escape special regex characters in a string.
75892
+ *
75893
+ * @param {string} str - String to escape
75894
+ * @returns {string} Escaped string safe for use in RegExp
75895
+ */
75896
+ static escapeRegex(str) {
75897
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75898
+ }
75899
+ /**
75900
+ * Convert a plain search string to a whitespace-flexible regex pattern.
75901
+ * This allows matching across paragraph boundaries.
75902
+ *
75903
+ * @param {string} searchString - The search string
75904
+ * @returns {string} Regex pattern string
75905
+ */
75906
+ static toFlexiblePattern(searchString) {
75907
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
75908
+ if (parts.length === 0) return "";
75909
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
75910
+ }
75911
+ /**
75912
+ * Search the index for matches.
75913
+ *
75914
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
75915
+ * @param {Object} options - Search options
75916
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
75917
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
75918
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
75919
+ */
75920
+ search(pattern, options = {}) {
75921
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
75922
+ const matches = [];
75923
+ let regex;
75924
+ if (pattern instanceof RegExp) {
75925
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
75926
+ regex = new RegExp(pattern.source, flags);
75927
+ } else if (typeof pattern === "string") {
75928
+ if (pattern.length === 0) return matches;
75929
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
75930
+ if (flexiblePattern.length === 0) return matches;
75931
+ const flags = caseSensitive ? "g" : "gi";
75932
+ regex = new RegExp(flexiblePattern, flags);
75933
+ } else {
75934
+ return matches;
75935
+ }
75936
+ let match;
75937
+ while ((match = regex.exec(this.text)) !== null && matches.length < maxMatches) {
75938
+ matches.push({
75939
+ start: match.index,
75940
+ end: match.index + match[0].length,
75941
+ text: match[0]
75942
+ });
75943
+ if (match[0].length === 0) {
75944
+ regex.lastIndex++;
75945
+ }
75946
+ }
75947
+ return matches;
75533
75948
  }
75534
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75535
- return tr.setMeta(searchKey, { query, range, highlight });
75536
75949
  }
75950
+ const customSearchHighlightsKey = new PluginKey("customSearchHighlights");
75537
75951
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
75952
+ const resolveInlineTextPosition = (doc2, position, direction) => {
75953
+ const docSize = doc2.content.size;
75954
+ if (!Number.isFinite(position) || position < 0 || position > docSize) {
75955
+ return position;
75956
+ }
75957
+ const step = direction === "forward" ? 1 : -1;
75958
+ let current = position;
75959
+ let iterations = 0;
75960
+ while (iterations < 8) {
75961
+ iterations += 1;
75962
+ const resolved = doc2.resolve(current);
75963
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
75964
+ if (!boundaryNode) break;
75965
+ if (boundaryNode.isText) break;
75966
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
75967
+ const next = current + step;
75968
+ if (next < 0 || next > docSize) break;
75969
+ current = next;
75970
+ const adjacent = doc2.resolve(current);
75971
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
75972
+ if (checkNode && checkNode.isText) break;
75973
+ }
75974
+ return current;
75975
+ };
75976
+ const resolveSearchRange = ({ doc: doc2, from: from3, to, expectedText, highlights }) => {
75977
+ const docSize = doc2.content.size;
75978
+ let resolvedFrom = Math.max(0, Math.min(from3, docSize));
75979
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
75980
+ if (highlights) {
75981
+ const windowStart = Math.max(0, resolvedFrom - 4);
75982
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
75983
+ const candidates = highlights.find(windowStart, windowEnd);
75984
+ if (candidates.length > 0) {
75985
+ let chosen = candidates[0];
75986
+ if (expectedText) {
75987
+ const matching = candidates.filter(
75988
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
75989
+ );
75990
+ if (matching.length > 0) {
75991
+ chosen = matching[0];
75992
+ }
75993
+ }
75994
+ resolvedFrom = chosen.from;
75995
+ resolvedTo = chosen.to;
75996
+ }
75997
+ }
75998
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
75999
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
76000
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
76001
+ resolvedFrom = normalizedFrom;
76002
+ resolvedTo = normalizedTo;
76003
+ }
76004
+ return { from: resolvedFrom, to: resolvedTo };
76005
+ };
76006
+ const getPositionTracker = (editor) => {
76007
+ if (!editor) return null;
76008
+ if (editor.positionTracker) return editor.positionTracker;
76009
+ const storageTracker = editor.storage?.positionTracker?.tracker;
76010
+ if (storageTracker) {
76011
+ editor.positionTracker = storageTracker;
76012
+ return storageTracker;
76013
+ }
76014
+ const tracker = new PositionTracker(editor);
76015
+ if (editor.storage?.positionTracker) {
76016
+ editor.storage.positionTracker.tracker = tracker;
76017
+ }
76018
+ editor.positionTracker = tracker;
76019
+ return tracker;
76020
+ };
75538
76021
  const Search = Extension.create({
75539
76022
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
75540
76023
  addStorage() {
@@ -75543,29 +76026,58 @@ const Search = Extension.create({
75543
76026
  * @private
75544
76027
  * @type {SearchMatch[]|null}
75545
76028
  */
75546
- searchResults: []
76029
+ searchResults: [],
76030
+ /**
76031
+ * @private
76032
+ * @type {boolean}
76033
+ * Whether to apply CSS highlight classes to matches
76034
+ */
76035
+ highlightEnabled: true,
76036
+ /**
76037
+ * @private
76038
+ * @type {SearchIndex}
76039
+ * Lazily-built search index for cross-paragraph matching
76040
+ */
76041
+ searchIndex: new SearchIndex()
75547
76042
  };
75548
76043
  },
75549
76044
  addPmPlugins() {
75550
76045
  const editor = this.editor;
75551
76046
  const storage = this.storage;
76047
+ const searchIndexInvalidatorPlugin = new Plugin({
76048
+ key: new PluginKey("searchIndexInvalidator"),
76049
+ appendTransaction(transactions, oldState, newState) {
76050
+ const docChanged = transactions.some((tr) => tr.docChanged);
76051
+ if (docChanged && storage?.searchIndex) {
76052
+ storage.searchIndex.invalidate();
76053
+ }
76054
+ return null;
76055
+ }
76056
+ });
75552
76057
  const searchHighlightWithIdPlugin = new Plugin({
75553
- key: new PluginKey("customSearchHighlights"),
76058
+ key: customSearchHighlightsKey,
75554
76059
  props: {
75555
76060
  decorations(state) {
75556
76061
  if (!editor) return null;
75557
76062
  const matches = storage?.searchResults;
75558
76063
  if (!matches?.length) return null;
75559
- const decorations = matches.map(
75560
- (match) => Decoration.inline(match.from, match.to, {
75561
- id: `search-match-${match.id}`
75562
- })
75563
- );
76064
+ const highlightEnabled = storage?.highlightEnabled !== false;
76065
+ const decorations = [];
76066
+ for (const match of matches) {
76067
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
76068
+ if (match.ranges && match.ranges.length > 0) {
76069
+ for (const range of match.ranges) {
76070
+ decorations.push(Decoration.inline(range.from, range.to, attrs));
76071
+ }
76072
+ } else {
76073
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
76074
+ }
76075
+ }
75564
76076
  return DecorationSet.create(state.doc, decorations);
75565
76077
  }
75566
76078
  }
75567
76079
  });
75568
- return [search(), searchHighlightWithIdPlugin];
76080
+ return [search(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
75569
76081
  },
75570
76082
  addCommands() {
75571
76083
  return {
@@ -75579,21 +76091,51 @@ const Search = Extension.create({
75579
76091
  goToFirstMatch: () => (
75580
76092
  /** @returns {boolean} */
75581
76093
  ({ state, editor, dispatch }) => {
76094
+ const searchResults = this.storage?.searchResults;
76095
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
76096
+ const firstMatch = searchResults[0];
76097
+ const from3 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
76098
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
76099
+ if (typeof from3 !== "number" || typeof to !== "number") {
76100
+ return false;
76101
+ }
76102
+ editor.view.focus();
76103
+ const tr2 = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
76104
+ if (dispatch) dispatch(tr2);
76105
+ const presentationEditor2 = editor.presentationEditor;
76106
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
76107
+ const didScroll = presentationEditor2.scrollToPosition(from3, { block: "center" });
76108
+ if (didScroll) return true;
76109
+ }
76110
+ try {
76111
+ const domPos = editor.view.domAtPos(from3);
76112
+ if (domPos?.node?.scrollIntoView) {
76113
+ domPos.node.scrollIntoView(true);
76114
+ }
76115
+ } catch {
76116
+ }
76117
+ return true;
76118
+ }
75582
76119
  const highlights = getMatchHighlights(state);
75583
76120
  if (!highlights) return false;
75584
76121
  const decorations = highlights.find();
75585
76122
  if (!decorations?.length) return false;
75586
- const firstMatch = decorations[0];
76123
+ const firstDeco = decorations[0];
75587
76124
  editor.view.focus();
75588
- const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
76125
+ const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
75589
76126
  if (dispatch) dispatch(tr);
75590
76127
  const presentationEditor = editor.presentationEditor;
75591
76128
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
75592
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
76129
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
75593
76130
  if (didScroll) return true;
75594
76131
  }
75595
- const domPos = editor.view.domAtPos(firstMatch.from);
75596
- domPos?.node?.scrollIntoView(true);
76132
+ try {
76133
+ const domPos = editor.view.domAtPos(firstDeco.from);
76134
+ if (domPos?.node?.scrollIntoView) {
76135
+ domPos.node.scrollIntoView(true);
76136
+ }
76137
+ } catch {
76138
+ }
75597
76139
  return true;
75598
76140
  }
75599
76141
  ),
@@ -75611,53 +76153,57 @@ const Search = Extension.create({
75611
76153
  *
75612
76154
  * // Search without visual highlighting
75613
76155
  * const silentMatches = editor.commands.search('test', { highlight: false })
75614
- * @note Returns array of SearchMatch objects with positions and IDs
76156
+ *
76157
+ * // Cross-paragraph search (works by default for plain strings)
76158
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
76159
+ * @note Returns array of SearchMatch objects with positions and IDs.
76160
+ * Plain string searches are whitespace-flexible and match across paragraphs.
76161
+ * Regex searches match exactly as specified.
75615
76162
  */
75616
76163
  search: (patternInput, options = {}) => (
75617
76164
  /** @returns {SearchMatch[]} */
75618
- ({ state, dispatch }) => {
76165
+ ({ state, dispatch, editor }) => {
75619
76166
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75620
76167
  throw new TypeError("Search options must be an object");
75621
76168
  }
75622
76169
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75623
- let pattern;
76170
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
75624
76171
  let caseSensitive = false;
75625
- let regexp = false;
75626
- const wholeWord = false;
76172
+ let searchPattern = patternInput;
75627
76173
  if (isRegExp(patternInput)) {
75628
- const regexPattern = (
75629
- /** @type {RegExp} */
75630
- patternInput
75631
- );
75632
- regexp = true;
75633
- pattern = regexPattern.source;
75634
- caseSensitive = !regexPattern.flags.includes("i");
76174
+ caseSensitive = !patternInput.flags.includes("i");
76175
+ searchPattern = patternInput;
75635
76176
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
75636
76177
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
75637
- regexp = true;
75638
- pattern = body;
75639
76178
  caseSensitive = !flags.includes("i");
76179
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
75640
76180
  } else {
75641
- pattern = String(patternInput);
76181
+ searchPattern = String(patternInput);
75642
76182
  }
75643
- const query = new SearchQuery({
75644
- search: pattern,
76183
+ const searchIndex = this.storage.searchIndex;
76184
+ searchIndex.ensureValid(state.doc);
76185
+ const indexMatches = searchIndex.search(searchPattern, {
75645
76186
  caseSensitive,
75646
- regexp,
75647
- wholeWord
76187
+ maxMatches
75648
76188
  });
75649
- const tr = setSearchState(state.tr, query, null, { highlight });
75650
- dispatch(tr);
75651
- const newState = state.apply(tr);
75652
- const decoSet = getMatchHighlights(newState);
75653
- const matches = decoSet ? decoSet.find() : [];
75654
- const resultMatches = matches.map((d) => ({
75655
- from: d.from,
75656
- to: d.to,
75657
- text: newState.doc.textBetween(d.from, d.to),
75658
- id: v4()
75659
- }));
76189
+ const resultMatches = [];
76190
+ for (const indexMatch of indexMatches) {
76191
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
76192
+ if (ranges.length === 0) continue;
76193
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
76194
+ const combinedText = matchTexts.join("");
76195
+ const match = {
76196
+ from: ranges[0].from,
76197
+ to: ranges[ranges.length - 1].to,
76198
+ text: combinedText,
76199
+ id: v4(),
76200
+ ranges,
76201
+ trackerIds: []
76202
+ };
76203
+ resultMatches.push(match);
76204
+ }
75660
76205
  this.storage.searchResults = resultMatches;
76206
+ this.storage.highlightEnabled = highlight;
75661
76207
  return resultMatches;
75662
76208
  }
75663
76209
  ),
@@ -75668,12 +76214,48 @@ const Search = Extension.create({
75668
76214
  * @example
75669
76215
  * const searchResults = editor.commands.search('test string')
75670
76216
  * editor.commands.goToSearchResult(searchResults[3])
75671
- * @note Scrolls to match and selects it
76217
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
76218
+ * selects the first range and scrolls to it.
75672
76219
  */
75673
76220
  goToSearchResult: (match) => (
75674
76221
  /** @returns {boolean} */
75675
76222
  ({ state, dispatch, editor }) => {
75676
- const { from: from3, to } = match;
76223
+ const positionTracker = getPositionTracker(editor);
76224
+ const doc2 = state.doc;
76225
+ const highlights = getMatchHighlights(state);
76226
+ let from3, to;
76227
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
76228
+ if (positionTracker?.resolve && match.trackerIds[0]) {
76229
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
76230
+ if (resolved) {
76231
+ from3 = resolved.from;
76232
+ to = resolved.to;
76233
+ }
76234
+ }
76235
+ if (from3 === void 0) {
76236
+ from3 = match.ranges[0].from;
76237
+ to = match.ranges[0].to;
76238
+ }
76239
+ } else {
76240
+ from3 = match.from;
76241
+ to = match.to;
76242
+ if (positionTracker?.resolve && match?.id) {
76243
+ const resolved = positionTracker.resolve(match.id);
76244
+ if (resolved) {
76245
+ from3 = resolved.from;
76246
+ to = resolved.to;
76247
+ }
76248
+ }
76249
+ }
76250
+ const normalized = resolveSearchRange({
76251
+ doc: doc2,
76252
+ from: from3,
76253
+ to,
76254
+ expectedText: match?.text ?? null,
76255
+ highlights
76256
+ });
76257
+ from3 = normalized.from;
76258
+ to = normalized.to;
75677
76259
  editor.view.focus();
75678
76260
  const tr = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
75679
76261
  if (dispatch) dispatch(tr);