@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,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-B4dnd1SO.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
  /**
@@ -13064,6 +13244,90 @@ const CommentsPlugin = Extension.create({
13064
13244
  name: "comments",
13065
13245
  addCommands() {
13066
13246
  return {
13247
+ /**
13248
+ * Add a comment to the current selection
13249
+ * @category Command
13250
+ * @param {string|Object} contentOrOptions - Comment content as a string, or an options object
13251
+ * @param {string} [contentOrOptions.content] - The comment content (text or HTML)
13252
+ * @param {string} [contentOrOptions.author] - Author name (defaults to user from editor config)
13253
+ * @param {string} [contentOrOptions.authorEmail] - Author email (defaults to user from editor config)
13254
+ * @param {string} [contentOrOptions.authorImage] - Author image URL (defaults to user from editor config)
13255
+ * @param {boolean} [contentOrOptions.isInternal=false] - Whether the comment is internal/private
13256
+ * @returns {boolean} True if the comment was added successfully, false otherwise
13257
+ * @example
13258
+ * // Simple usage with just content
13259
+ * editor.commands.addComment('This needs review')
13260
+ *
13261
+ * // With options
13262
+ * editor.commands.addComment({
13263
+ * content: 'Please clarify this section',
13264
+ * author: 'Jane Doe',
13265
+ * isInternal: true
13266
+ * })
13267
+ *
13268
+ * // To get the comment ID, listen to the commentsUpdate event
13269
+ * editor.on('commentsUpdate', (event) => {
13270
+ * if (event.type === 'add') {
13271
+ * console.log('New comment ID:', event.activeCommentId)
13272
+ * }
13273
+ * })
13274
+ */
13275
+ addComment: (contentOrOptions) => ({ tr, dispatch, editor }) => {
13276
+ const { selection } = tr;
13277
+ const { $from, $to } = selection;
13278
+ if ($from.pos === $to.pos) {
13279
+ console.warn("addComment requires a text selection. Please select text before adding a comment.");
13280
+ return false;
13281
+ }
13282
+ let content, author, authorEmail, authorImage, isInternal;
13283
+ if (typeof contentOrOptions === "string") {
13284
+ content = contentOrOptions;
13285
+ } else if (contentOrOptions && typeof contentOrOptions === "object") {
13286
+ content = contentOrOptions.content;
13287
+ author = contentOrOptions.author;
13288
+ authorEmail = contentOrOptions.authorEmail;
13289
+ authorImage = contentOrOptions.authorImage;
13290
+ isInternal = contentOrOptions.isInternal;
13291
+ }
13292
+ const commentId = v4();
13293
+ const resolvedInternal = isInternal ?? false;
13294
+ const configUser = editor.options?.user || {};
13295
+ tr.setMeta(CommentsPluginKey, { event: "add" });
13296
+ tr.addMark(
13297
+ $from.pos,
13298
+ $to.pos,
13299
+ editor.schema.marks[CommentMarkName$1].create({
13300
+ commentId,
13301
+ internal: resolvedInternal
13302
+ })
13303
+ );
13304
+ if (dispatch) dispatch(tr);
13305
+ const commentPayload = normalizeCommentEventPayload({
13306
+ conversation: {
13307
+ commentId,
13308
+ isInternal: resolvedInternal,
13309
+ commentText: content,
13310
+ creatorName: author ?? configUser.name,
13311
+ creatorEmail: authorEmail ?? configUser.email,
13312
+ creatorImage: authorImage ?? configUser.image,
13313
+ createdTime: Date.now()
13314
+ },
13315
+ editorOptions: editor.options,
13316
+ fallbackCommentId: commentId,
13317
+ fallbackInternal: resolvedInternal
13318
+ });
13319
+ editor.emit("commentsUpdate", {
13320
+ type: comments_module_events.ADD,
13321
+ comment: commentPayload,
13322
+ activeCommentId: commentId
13323
+ });
13324
+ return true;
13325
+ },
13326
+ /**
13327
+ * @private
13328
+ * Internal command to insert a comment mark at the current selection.
13329
+ * Use `addComment` for the public API.
13330
+ */
13067
13331
  insertComment: (conversation = {}) => ({ tr, dispatch }) => {
13068
13332
  const { selection } = tr;
13069
13333
  const { $from, $to } = selection;
@@ -15507,7 +15771,7 @@ const canUseDOM = () => {
15507
15771
  return false;
15508
15772
  }
15509
15773
  };
15510
- const summaryVersion = "1.6.0-next.1";
15774
+ const summaryVersion = "1.6.0-next.3";
15511
15775
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
15512
15776
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
15513
15777
  function mapAttributes(attrs) {
@@ -17000,7 +17264,7 @@ class Editor extends EventEmitter {
17000
17264
  */
17001
17265
  #createExtensionService() {
17002
17266
  const allowedExtensions = ["extension", "node", "mark"];
17003
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
17267
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
17004
17268
  const externalExtensions = this.options.externalExtensions || [];
17005
17269
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
17006
17270
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -18174,7 +18438,7 @@ class Editor extends EventEmitter {
18174
18438
  * Process collaboration migrations
18175
18439
  */
18176
18440
  processCollaborationMigrations() {
18177
- console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.1");
18441
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.3");
18178
18442
  if (!this.options.ydoc) return;
18179
18443
  const metaMap = this.options.ydoc.getMap("meta");
18180
18444
  let docVersion = metaMap.get("version");
@@ -75527,14 +75791,317 @@ function getMatchHighlights(state) {
75527
75791
  let search2 = searchKey.getState(state);
75528
75792
  return search2 ? search2.deco : DecorationSet.empty;
75529
75793
  }
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");
75794
+ const BLOCK_SEPARATOR = "\n";
75795
+ const ATOM_PLACEHOLDER = "";
75796
+ class SearchIndex {
75797
+ /** @type {string} */
75798
+ text = "";
75799
+ /** @type {Segment[]} */
75800
+ segments = [];
75801
+ /** @type {boolean} */
75802
+ valid = false;
75803
+ /** @type {number} */
75804
+ docSize = 0;
75805
+ /**
75806
+ * Build the search index from a ProseMirror document.
75807
+ * Uses doc.textBetween for the flattened string and walks
75808
+ * the document to build the segment offset map.
75809
+ *
75810
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
75811
+ */
75812
+ build(doc2) {
75813
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
75814
+ this.segments = [];
75815
+ this.docSize = doc2.content.size;
75816
+ let offset2 = 0;
75817
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
75818
+ this.segments.push(segment);
75819
+ offset2 = segment.offsetEnd;
75820
+ });
75821
+ this.valid = true;
75822
+ }
75823
+ /**
75824
+ * Walk the content of a node to build segments.
75825
+ * This method processes the children of a node, given the position
75826
+ * where the node's content starts.
75827
+ *
75828
+ * @param {import('prosemirror-model').Node} node - Current node
75829
+ * @param {number} contentStart - Document position where this node's content starts
75830
+ * @param {number} offset - Current offset in flattened string
75831
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75832
+ * @returns {number} The new offset after processing this node's content
75833
+ */
75834
+ #walkNodeContent(node, contentStart, offset2, addSegment) {
75835
+ let currentOffset = offset2;
75836
+ let isFirstChild = true;
75837
+ node.forEach((child, childContentOffset) => {
75838
+ const childDocPos = contentStart + childContentOffset;
75839
+ if (child.isBlock && !isFirstChild) {
75840
+ addSegment({
75841
+ offsetStart: currentOffset,
75842
+ offsetEnd: currentOffset + 1,
75843
+ docFrom: childDocPos,
75844
+ docTo: childDocPos,
75845
+ kind: "blockSep"
75846
+ });
75847
+ currentOffset += 1;
75848
+ }
75849
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
75850
+ isFirstChild = false;
75851
+ });
75852
+ return currentOffset;
75853
+ }
75854
+ /**
75855
+ * Recursively walk a node and its descendants to build segments.
75856
+ *
75857
+ * @param {import('prosemirror-model').Node} node - Current node
75858
+ * @param {number} docPos - Document position at start of this node
75859
+ * @param {number} offset - Current offset in flattened string
75860
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75861
+ * @returns {number} The new offset after processing this node
75862
+ */
75863
+ #walkNode(node, docPos, offset2, addSegment) {
75864
+ if (node.isText) {
75865
+ const text = node.text || "";
75866
+ if (text.length > 0) {
75867
+ addSegment({
75868
+ offsetStart: offset2,
75869
+ offsetEnd: offset2 + text.length,
75870
+ docFrom: docPos,
75871
+ docTo: docPos + text.length,
75872
+ kind: "text"
75873
+ });
75874
+ return offset2 + text.length;
75875
+ }
75876
+ return offset2;
75877
+ }
75878
+ if (node.isLeaf) {
75879
+ if (node.type.name === "hard_break") {
75880
+ addSegment({
75881
+ offsetStart: offset2,
75882
+ offsetEnd: offset2 + 1,
75883
+ docFrom: docPos,
75884
+ docTo: docPos + node.nodeSize,
75885
+ kind: "hardBreak"
75886
+ });
75887
+ return offset2 + 1;
75888
+ }
75889
+ addSegment({
75890
+ offsetStart: offset2,
75891
+ offsetEnd: offset2 + 1,
75892
+ docFrom: docPos,
75893
+ docTo: docPos + node.nodeSize,
75894
+ kind: "atom"
75895
+ });
75896
+ return offset2 + 1;
75897
+ }
75898
+ return this.#walkNodeContent(node, docPos + 1, offset2, addSegment);
75899
+ }
75900
+ /**
75901
+ * Mark the index as stale. It will be rebuilt on next search.
75902
+ */
75903
+ invalidate() {
75904
+ this.valid = false;
75905
+ }
75906
+ /**
75907
+ * Check if the index needs rebuilding for the given document.
75908
+ *
75909
+ * @param {import('prosemirror-model').Node} doc - The document to check against
75910
+ * @returns {boolean} True if index is stale and needs rebuilding
75911
+ */
75912
+ isStale(doc2) {
75913
+ return !this.valid || doc2.content.size !== this.docSize;
75914
+ }
75915
+ /**
75916
+ * Ensure the index is valid for the given document.
75917
+ * Rebuilds if stale.
75918
+ *
75919
+ * @param {import('prosemirror-model').Node} doc - The document
75920
+ */
75921
+ ensureValid(doc2) {
75922
+ if (this.isStale(doc2)) {
75923
+ this.build(doc2);
75924
+ }
75925
+ }
75926
+ /**
75927
+ * Convert an offset range in the flattened string to document ranges.
75928
+ * Skips separator/atom segments and returns only text ranges.
75929
+ *
75930
+ * @param {number} start - Start offset in flattened string
75931
+ * @param {number} end - End offset in flattened string
75932
+ * @returns {DocRange[]} Array of document ranges (text segments only)
75933
+ */
75934
+ offsetRangeToDocRanges(start2, end2) {
75935
+ const ranges = [];
75936
+ for (const segment of this.segments) {
75937
+ if (segment.offsetEnd <= start2) continue;
75938
+ if (segment.offsetStart >= end2) break;
75939
+ if (segment.kind !== "text") continue;
75940
+ const overlapStart = Math.max(start2, segment.offsetStart);
75941
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
75942
+ if (overlapStart < overlapEnd) {
75943
+ const startInSegment = overlapStart - segment.offsetStart;
75944
+ const endInSegment = overlapEnd - segment.offsetStart;
75945
+ ranges.push({
75946
+ from: segment.docFrom + startInSegment,
75947
+ to: segment.docFrom + endInSegment
75948
+ });
75949
+ }
75950
+ }
75951
+ return ranges;
75952
+ }
75953
+ /**
75954
+ * Find the document position for a given offset in the flattened string.
75955
+ *
75956
+ * @param {number} offset - Offset in flattened string
75957
+ * @returns {number|null} Document position, or null if not found
75958
+ */
75959
+ offsetToDocPos(offset2) {
75960
+ for (const segment of this.segments) {
75961
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
75962
+ if (segment.kind === "text") {
75963
+ return segment.docFrom + (offset2 - segment.offsetStart);
75964
+ }
75965
+ return segment.docFrom;
75966
+ }
75967
+ }
75968
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
75969
+ const lastSeg = this.segments[this.segments.length - 1];
75970
+ return lastSeg.docTo;
75971
+ }
75972
+ return null;
75973
+ }
75974
+ /**
75975
+ * Escape special regex characters in a string.
75976
+ *
75977
+ * @param {string} str - String to escape
75978
+ * @returns {string} Escaped string safe for use in RegExp
75979
+ */
75980
+ static escapeRegex(str) {
75981
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75982
+ }
75983
+ /**
75984
+ * Convert a plain search string to a whitespace-flexible regex pattern.
75985
+ * This allows matching across paragraph boundaries.
75986
+ *
75987
+ * @param {string} searchString - The search string
75988
+ * @returns {string} Regex pattern string
75989
+ */
75990
+ static toFlexiblePattern(searchString) {
75991
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
75992
+ if (parts.length === 0) return "";
75993
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
75994
+ }
75995
+ /**
75996
+ * Search the index for matches.
75997
+ *
75998
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
75999
+ * @param {Object} options - Search options
76000
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
76001
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
76002
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
76003
+ */
76004
+ search(pattern, options = {}) {
76005
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
76006
+ const matches = [];
76007
+ let regex;
76008
+ if (pattern instanceof RegExp) {
76009
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
76010
+ regex = new RegExp(pattern.source, flags);
76011
+ } else if (typeof pattern === "string") {
76012
+ if (pattern.length === 0) return matches;
76013
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
76014
+ if (flexiblePattern.length === 0) return matches;
76015
+ const flags = caseSensitive ? "g" : "gi";
76016
+ regex = new RegExp(flexiblePattern, flags);
76017
+ } else {
76018
+ return matches;
76019
+ }
76020
+ let match;
76021
+ while ((match = regex.exec(this.text)) !== null && matches.length < maxMatches) {
76022
+ matches.push({
76023
+ start: match.index,
76024
+ end: match.index + match[0].length,
76025
+ text: match[0]
76026
+ });
76027
+ if (match[0].length === 0) {
76028
+ regex.lastIndex++;
76029
+ }
76030
+ }
76031
+ return matches;
75533
76032
  }
75534
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75535
- return tr.setMeta(searchKey, { query, range, highlight });
75536
76033
  }
76034
+ const customSearchHighlightsKey = new PluginKey("customSearchHighlights");
75537
76035
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
76036
+ const resolveInlineTextPosition = (doc2, position, direction) => {
76037
+ const docSize = doc2.content.size;
76038
+ if (!Number.isFinite(position) || position < 0 || position > docSize) {
76039
+ return position;
76040
+ }
76041
+ const step = direction === "forward" ? 1 : -1;
76042
+ let current = position;
76043
+ let iterations = 0;
76044
+ while (iterations < 8) {
76045
+ iterations += 1;
76046
+ const resolved = doc2.resolve(current);
76047
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
76048
+ if (!boundaryNode) break;
76049
+ if (boundaryNode.isText) break;
76050
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
76051
+ const next = current + step;
76052
+ if (next < 0 || next > docSize) break;
76053
+ current = next;
76054
+ const adjacent = doc2.resolve(current);
76055
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
76056
+ if (checkNode && checkNode.isText) break;
76057
+ }
76058
+ return current;
76059
+ };
76060
+ const resolveSearchRange = ({ doc: doc2, from: from3, to, expectedText, highlights }) => {
76061
+ const docSize = doc2.content.size;
76062
+ let resolvedFrom = Math.max(0, Math.min(from3, docSize));
76063
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
76064
+ if (highlights) {
76065
+ const windowStart = Math.max(0, resolvedFrom - 4);
76066
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
76067
+ const candidates = highlights.find(windowStart, windowEnd);
76068
+ if (candidates.length > 0) {
76069
+ let chosen = candidates[0];
76070
+ if (expectedText) {
76071
+ const matching = candidates.filter(
76072
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
76073
+ );
76074
+ if (matching.length > 0) {
76075
+ chosen = matching[0];
76076
+ }
76077
+ }
76078
+ resolvedFrom = chosen.from;
76079
+ resolvedTo = chosen.to;
76080
+ }
76081
+ }
76082
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
76083
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
76084
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
76085
+ resolvedFrom = normalizedFrom;
76086
+ resolvedTo = normalizedTo;
76087
+ }
76088
+ return { from: resolvedFrom, to: resolvedTo };
76089
+ };
76090
+ const getPositionTracker = (editor) => {
76091
+ if (!editor) return null;
76092
+ if (editor.positionTracker) return editor.positionTracker;
76093
+ const storageTracker = editor.storage?.positionTracker?.tracker;
76094
+ if (storageTracker) {
76095
+ editor.positionTracker = storageTracker;
76096
+ return storageTracker;
76097
+ }
76098
+ const tracker = new PositionTracker(editor);
76099
+ if (editor.storage?.positionTracker) {
76100
+ editor.storage.positionTracker.tracker = tracker;
76101
+ }
76102
+ editor.positionTracker = tracker;
76103
+ return tracker;
76104
+ };
75538
76105
  const Search = Extension.create({
75539
76106
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
75540
76107
  addStorage() {
@@ -75543,29 +76110,58 @@ const Search = Extension.create({
75543
76110
  * @private
75544
76111
  * @type {SearchMatch[]|null}
75545
76112
  */
75546
- searchResults: []
76113
+ searchResults: [],
76114
+ /**
76115
+ * @private
76116
+ * @type {boolean}
76117
+ * Whether to apply CSS highlight classes to matches
76118
+ */
76119
+ highlightEnabled: true,
76120
+ /**
76121
+ * @private
76122
+ * @type {SearchIndex}
76123
+ * Lazily-built search index for cross-paragraph matching
76124
+ */
76125
+ searchIndex: new SearchIndex()
75547
76126
  };
75548
76127
  },
75549
76128
  addPmPlugins() {
75550
76129
  const editor = this.editor;
75551
76130
  const storage = this.storage;
76131
+ const searchIndexInvalidatorPlugin = new Plugin({
76132
+ key: new PluginKey("searchIndexInvalidator"),
76133
+ appendTransaction(transactions, oldState, newState) {
76134
+ const docChanged = transactions.some((tr) => tr.docChanged);
76135
+ if (docChanged && storage?.searchIndex) {
76136
+ storage.searchIndex.invalidate();
76137
+ }
76138
+ return null;
76139
+ }
76140
+ });
75552
76141
  const searchHighlightWithIdPlugin = new Plugin({
75553
- key: new PluginKey("customSearchHighlights"),
76142
+ key: customSearchHighlightsKey,
75554
76143
  props: {
75555
76144
  decorations(state) {
75556
76145
  if (!editor) return null;
75557
76146
  const matches = storage?.searchResults;
75558
76147
  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
- );
76148
+ const highlightEnabled = storage?.highlightEnabled !== false;
76149
+ const decorations = [];
76150
+ for (const match of matches) {
76151
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
76152
+ if (match.ranges && match.ranges.length > 0) {
76153
+ for (const range of match.ranges) {
76154
+ decorations.push(Decoration.inline(range.from, range.to, attrs));
76155
+ }
76156
+ } else {
76157
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
76158
+ }
76159
+ }
75564
76160
  return DecorationSet.create(state.doc, decorations);
75565
76161
  }
75566
76162
  }
75567
76163
  });
75568
- return [search(), searchHighlightWithIdPlugin];
76164
+ return [search(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
75569
76165
  },
75570
76166
  addCommands() {
75571
76167
  return {
@@ -75579,21 +76175,51 @@ const Search = Extension.create({
75579
76175
  goToFirstMatch: () => (
75580
76176
  /** @returns {boolean} */
75581
76177
  ({ state, editor, dispatch }) => {
76178
+ const searchResults = this.storage?.searchResults;
76179
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
76180
+ const firstMatch = searchResults[0];
76181
+ const from3 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
76182
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
76183
+ if (typeof from3 !== "number" || typeof to !== "number") {
76184
+ return false;
76185
+ }
76186
+ editor.view.focus();
76187
+ const tr2 = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
76188
+ if (dispatch) dispatch(tr2);
76189
+ const presentationEditor2 = editor.presentationEditor;
76190
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
76191
+ const didScroll = presentationEditor2.scrollToPosition(from3, { block: "center" });
76192
+ if (didScroll) return true;
76193
+ }
76194
+ try {
76195
+ const domPos = editor.view.domAtPos(from3);
76196
+ if (domPos?.node?.scrollIntoView) {
76197
+ domPos.node.scrollIntoView(true);
76198
+ }
76199
+ } catch {
76200
+ }
76201
+ return true;
76202
+ }
75582
76203
  const highlights = getMatchHighlights(state);
75583
76204
  if (!highlights) return false;
75584
76205
  const decorations = highlights.find();
75585
76206
  if (!decorations?.length) return false;
75586
- const firstMatch = decorations[0];
76207
+ const firstDeco = decorations[0];
75587
76208
  editor.view.focus();
75588
- const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
76209
+ const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
75589
76210
  if (dispatch) dispatch(tr);
75590
76211
  const presentationEditor = editor.presentationEditor;
75591
76212
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
75592
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
76213
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
75593
76214
  if (didScroll) return true;
75594
76215
  }
75595
- const domPos = editor.view.domAtPos(firstMatch.from);
75596
- domPos?.node?.scrollIntoView(true);
76216
+ try {
76217
+ const domPos = editor.view.domAtPos(firstDeco.from);
76218
+ if (domPos?.node?.scrollIntoView) {
76219
+ domPos.node.scrollIntoView(true);
76220
+ }
76221
+ } catch {
76222
+ }
75597
76223
  return true;
75598
76224
  }
75599
76225
  ),
@@ -75611,53 +76237,57 @@ const Search = Extension.create({
75611
76237
  *
75612
76238
  * // Search without visual highlighting
75613
76239
  * const silentMatches = editor.commands.search('test', { highlight: false })
75614
- * @note Returns array of SearchMatch objects with positions and IDs
76240
+ *
76241
+ * // Cross-paragraph search (works by default for plain strings)
76242
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
76243
+ * @note Returns array of SearchMatch objects with positions and IDs.
76244
+ * Plain string searches are whitespace-flexible and match across paragraphs.
76245
+ * Regex searches match exactly as specified.
75615
76246
  */
75616
76247
  search: (patternInput, options = {}) => (
75617
76248
  /** @returns {SearchMatch[]} */
75618
- ({ state, dispatch }) => {
76249
+ ({ state, dispatch, editor }) => {
75619
76250
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
75620
76251
  throw new TypeError("Search options must be an object");
75621
76252
  }
75622
76253
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
75623
- let pattern;
76254
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
75624
76255
  let caseSensitive = false;
75625
- let regexp = false;
75626
- const wholeWord = false;
76256
+ let searchPattern = patternInput;
75627
76257
  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");
76258
+ caseSensitive = !patternInput.flags.includes("i");
76259
+ searchPattern = patternInput;
75635
76260
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
75636
76261
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
75637
- regexp = true;
75638
- pattern = body;
75639
76262
  caseSensitive = !flags.includes("i");
76263
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
75640
76264
  } else {
75641
- pattern = String(patternInput);
76265
+ searchPattern = String(patternInput);
75642
76266
  }
75643
- const query = new SearchQuery({
75644
- search: pattern,
76267
+ const searchIndex = this.storage.searchIndex;
76268
+ searchIndex.ensureValid(state.doc);
76269
+ const indexMatches = searchIndex.search(searchPattern, {
75645
76270
  caseSensitive,
75646
- regexp,
75647
- wholeWord
76271
+ maxMatches
75648
76272
  });
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
- }));
76273
+ const resultMatches = [];
76274
+ for (const indexMatch of indexMatches) {
76275
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
76276
+ if (ranges.length === 0) continue;
76277
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
76278
+ const combinedText = matchTexts.join("");
76279
+ const match = {
76280
+ from: ranges[0].from,
76281
+ to: ranges[ranges.length - 1].to,
76282
+ text: combinedText,
76283
+ id: v4(),
76284
+ ranges,
76285
+ trackerIds: []
76286
+ };
76287
+ resultMatches.push(match);
76288
+ }
75660
76289
  this.storage.searchResults = resultMatches;
76290
+ this.storage.highlightEnabled = highlight;
75661
76291
  return resultMatches;
75662
76292
  }
75663
76293
  ),
@@ -75668,12 +76298,48 @@ const Search = Extension.create({
75668
76298
  * @example
75669
76299
  * const searchResults = editor.commands.search('test string')
75670
76300
  * editor.commands.goToSearchResult(searchResults[3])
75671
- * @note Scrolls to match and selects it
76301
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
76302
+ * selects the first range and scrolls to it.
75672
76303
  */
75673
76304
  goToSearchResult: (match) => (
75674
76305
  /** @returns {boolean} */
75675
76306
  ({ state, dispatch, editor }) => {
75676
- const { from: from3, to } = match;
76307
+ const positionTracker = getPositionTracker(editor);
76308
+ const doc2 = state.doc;
76309
+ const highlights = getMatchHighlights(state);
76310
+ let from3, to;
76311
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
76312
+ if (positionTracker?.resolve && match.trackerIds[0]) {
76313
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
76314
+ if (resolved) {
76315
+ from3 = resolved.from;
76316
+ to = resolved.to;
76317
+ }
76318
+ }
76319
+ if (from3 === void 0) {
76320
+ from3 = match.ranges[0].from;
76321
+ to = match.ranges[0].to;
76322
+ }
76323
+ } else {
76324
+ from3 = match.from;
76325
+ to = match.to;
76326
+ if (positionTracker?.resolve && match?.id) {
76327
+ const resolved = positionTracker.resolve(match.id);
76328
+ if (resolved) {
76329
+ from3 = resolved.from;
76330
+ to = resolved.to;
76331
+ }
76332
+ }
76333
+ }
76334
+ const normalized = resolveSearchRange({
76335
+ doc: doc2,
76336
+ from: from3,
76337
+ to,
76338
+ expectedText: match?.text ?? null,
76339
+ highlights
76340
+ });
76341
+ from3 = normalized.from;
76342
+ to = normalized.to;
75677
76343
  editor.view.focus();
75678
76344
  const tr = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
75679
76345
  if (dispatch) dispatch(tr);