@tiptap/extension-find-and-replace 3.29.0

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.
Files changed (58) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/index.cjs +950 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +230 -0
  6. package/dist/index.d.ts +230 -0
  7. package/dist/index.js +915 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +55 -0
  10. package/src/constants/constants.ts +2 -0
  11. package/src/find-and-replace.ts +434 -0
  12. package/src/index.ts +8 -0
  13. package/src/plugin/plugin-state.ts +27 -0
  14. package/src/plugin/plugin.ts +60 -0
  15. package/src/search/matches.ts +26 -0
  16. package/src/search/regex.ts +67 -0
  17. package/src/search/search.ts +70 -0
  18. package/src/search/text-segments.ts +63 -0
  19. package/src/search/textblock-search.ts +31 -0
  20. package/src/search/types.ts +17 -0
  21. package/src/state/debounced-search-state.ts +39 -0
  22. package/src/style/create-style-tag.ts +23 -0
  23. package/src/style/style.ts +10 -0
  24. package/src/types.ts +96 -0
  25. package/src/utils/createDecoration.ts +14 -0
  26. package/src/utils/createDecorations.ts +16 -0
  27. package/src/utils/createState.ts +23 -0
  28. package/src/utils/findDecorations.ts +27 -0
  29. package/src/utils/getChangedTextblocks.ts +29 -0
  30. package/src/utils/getFallbackCurrentIndex.ts +8 -0
  31. package/src/utils/getMappedCurrentIndex.ts +32 -0
  32. package/src/utils/getMetaCurrentIndex.ts +19 -0
  33. package/src/utils/getPreviousCurrentResult.ts +10 -0
  34. package/src/utils/getResultsToRefresh.ts +22 -0
  35. package/src/utils/getSearchCurrentIndex.ts +24 -0
  36. package/src/utils/getTextNodeResult.ts +21 -0
  37. package/src/utils/groupResults.ts +68 -0
  38. package/src/utils/hasCurrentIndex.ts +5 -0
  39. package/src/utils/index.ts +2 -0
  40. package/src/utils/isCurrentResult.ts +7 -0
  41. package/src/utils/isNewSearch.ts +5 -0
  42. package/src/utils/mapResult.ts +10 -0
  43. package/src/utils/mapResults.ts +19 -0
  44. package/src/utils/mergeResults.ts +18 -0
  45. package/src/utils/normalizeCurrentIndex.ts +10 -0
  46. package/src/utils/refreshCurrentIndexState.ts +30 -0
  47. package/src/utils/refreshDecorations.ts +28 -0
  48. package/src/utils/refreshDocumentState.ts +47 -0
  49. package/src/utils/refreshState.ts +27 -0
  50. package/src/utils/replaceAllResults.ts +18 -0
  51. package/src/utils/replaceGroup.ts +54 -0
  52. package/src/utils/resolveCurrentIndex.ts +27 -0
  53. package/src/utils/resultInTextblocks.ts +30 -0
  54. package/src/utils/sameResult.ts +5 -0
  55. package/src/utils/shouldRefreshResult.ts +18 -0
  56. package/src/utils/touchesSearch.ts +7 -0
  57. package/src/utils/types.ts +11 -0
  58. package/src/utils/updateState.ts +33 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,950 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FindAndReplace: () => FindAndReplace,
24
+ FindAndReplacePlugin: () => FindAndReplacePlugin,
25
+ FindAndReplacePluginKey: () => FindAndReplacePluginKey,
26
+ createSearchRegex: () => createSearchRegex,
27
+ currentResultClass: () => currentResultClass,
28
+ default: () => index_default,
29
+ findNextIndex: () => findNextIndex,
30
+ resultClass: () => resultClass,
31
+ searchDocument: () => searchDocument,
32
+ searchTextblocks: () => searchTextblocks
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/find-and-replace.ts
37
+ var import_core = require("@tiptap/core");
38
+ var import_state2 = require("@tiptap/pm/state");
39
+
40
+ // src/state/debounced-search-state.ts
41
+ var debouncedSearchState = /* @__PURE__ */ new WeakMap();
42
+ function getDebouncedSearchState(editor) {
43
+ const existingState = debouncedSearchState.get(editor);
44
+ if (existingState) {
45
+ return existingState;
46
+ }
47
+ const nextState = {
48
+ timeout: null,
49
+ pendingTerm: null
50
+ };
51
+ debouncedSearchState.set(editor, nextState);
52
+ return nextState;
53
+ }
54
+ function clearDebouncedSearchState(editor) {
55
+ const state = debouncedSearchState.get(editor);
56
+ if (!state) {
57
+ return;
58
+ }
59
+ if (state.timeout !== null) {
60
+ clearTimeout(state.timeout);
61
+ }
62
+ debouncedSearchState.delete(editor);
63
+ }
64
+
65
+ // src/plugin/plugin.ts
66
+ var import_state = require("@tiptap/pm/state");
67
+
68
+ // src/constants/constants.ts
69
+ var resultClass = "find-and-replace-result";
70
+ var currentResultClass = "find-and-replace-result-current";
71
+
72
+ // src/style/create-style-tag.ts
73
+ var createStyleTag = (style2, nonce) => {
74
+ const existing = document.querySelector(
75
+ "style[data-tiptap-extension-find-and-replace-style]"
76
+ );
77
+ if (existing !== null) {
78
+ return existing;
79
+ }
80
+ const styleNode = document.createElement("style");
81
+ if (nonce) {
82
+ styleNode.setAttribute("nonce", nonce);
83
+ }
84
+ styleNode.setAttribute("data-tiptap-extension-find-and-replace-style", "");
85
+ styleNode.textContent = style2;
86
+ document.getElementsByTagName("head")[0].appendChild(styleNode);
87
+ return styleNode;
88
+ };
89
+
90
+ // src/style/style.ts
91
+ var style = `.find-and-replace-result {
92
+ background-color: rgb(255 225 0 / 0.4);
93
+ }
94
+
95
+ .find-and-replace-result-current {
96
+ background-color: rgb(255 165 0 / 0.55);
97
+ }
98
+ `;
99
+
100
+ // src/search/regex.ts
101
+ var import_re2js = require("re2js");
102
+ var escapeRegExp = (value) => {
103
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
104
+ };
105
+ var unicodeWordCharacter = "[\\p{L}\\p{M}\\p{N}\\p{Pc}]";
106
+ function compileNativeRegex(source, caseSensitive) {
107
+ try {
108
+ return new RegExp(source, caseSensitive ? "gu" : "giu");
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ function compileSafeRegex(source, caseSensitive) {
114
+ try {
115
+ const flags = caseSensitive ? 0 : import_re2js.RE2JS.CASE_INSENSITIVE;
116
+ return import_re2js.RE2JS.compile(source, flags);
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ function compileRegex(source, options) {
122
+ return options.useRegex ? compileSafeRegex(source, options.caseSensitive) : compileNativeRegex(source, options.caseSensitive);
123
+ }
124
+ function createSearchRegex(term, options) {
125
+ if (!term) {
126
+ return null;
127
+ }
128
+ let source;
129
+ if (options.useRegex) {
130
+ source = term;
131
+ } else {
132
+ const escaped = escapeRegExp(term);
133
+ source = options.wholeWord ? `(?<!${unicodeWordCharacter})${escaped}(?!${unicodeWordCharacter})` : escaped;
134
+ }
135
+ return compileRegex(source, options);
136
+ }
137
+
138
+ // src/search/matches.ts
139
+ function* findNativeMatches(regex, text) {
140
+ for (const match of text.matchAll(regex)) {
141
+ yield { index: match.index, value: match[0] };
142
+ }
143
+ }
144
+ function* findSafeMatches(regex, text) {
145
+ var _a;
146
+ const matcher = regex.matcher(text);
147
+ while (matcher.find()) {
148
+ yield { index: matcher.start(), value: (_a = matcher.group()) != null ? _a : "" };
149
+ }
150
+ }
151
+ function findMatches(regex, text) {
152
+ return regex instanceof RegExp ? findNativeMatches(regex, text) : findSafeMatches(regex, text);
153
+ }
154
+
155
+ // src/search/text-segments.ts
156
+ function getTextSegments(textblock, pos) {
157
+ const segments = [];
158
+ let textOffset = 0;
159
+ textblock.forEach((child, offset) => {
160
+ var _a;
161
+ const text = child.isText ? (_a = child.text) != null ? _a : "" : "\n";
162
+ segments.push({
163
+ isText: child.isText,
164
+ pos: pos + 1 + offset,
165
+ length: child.nodeSize,
166
+ text,
167
+ textOffset
168
+ });
169
+ textOffset += text.length;
170
+ });
171
+ return segments;
172
+ }
173
+ function findOffsetSegment(segments, offset) {
174
+ var _a;
175
+ let low = 0;
176
+ let high = segments.length;
177
+ while (low < high) {
178
+ const middle = Math.floor((low + high) / 2);
179
+ const segment = segments[middle];
180
+ const segmentEnd = segment.textOffset + segment.text.length;
181
+ if (offset < segmentEnd) {
182
+ high = middle;
183
+ } else {
184
+ low = middle + 1;
185
+ }
186
+ }
187
+ return (_a = segments[low]) != null ? _a : segments.at(-1);
188
+ }
189
+ function offsetToPos(segments, offset) {
190
+ const segment = findOffsetSegment(segments, offset);
191
+ return segment ? segment.pos + Math.min(offset - segment.textOffset, segment.length) : 0;
192
+ }
193
+ function overlapsNonTextSegment(segments, from, to) {
194
+ return segments.some(
195
+ (segment) => !segment.isText && from < segment.textOffset + segment.text.length && to > segment.textOffset
196
+ );
197
+ }
198
+
199
+ // src/search/textblock-search.ts
200
+ function searchTextblock(regex, textblock, pos) {
201
+ const segments = getTextSegments(textblock, pos);
202
+ const text = segments.map((segment) => segment.text).join("");
203
+ const results = [];
204
+ for (const match of findMatches(regex, text)) {
205
+ if (match.value.length === 0) {
206
+ continue;
207
+ }
208
+ const matchEnd = match.index + match.value.length;
209
+ if (overlapsNonTextSegment(segments, match.index, matchEnd)) {
210
+ continue;
211
+ }
212
+ results.push({
213
+ from: offsetToPos(segments, match.index),
214
+ to: offsetToPos(segments, matchEnd)
215
+ });
216
+ }
217
+ return results;
218
+ }
219
+
220
+ // src/search/search.ts
221
+ function searchTextblocks(textblocks, term, options) {
222
+ const regex = createSearchRegex(term, options);
223
+ if (!regex) {
224
+ return [];
225
+ }
226
+ return textblocks.flatMap(({ node, pos }) => searchTextblock(regex, node, pos));
227
+ }
228
+ function searchDocument(doc, term, options) {
229
+ const textblocks = [];
230
+ doc.descendants((node, pos) => {
231
+ if (!node.isTextblock) {
232
+ return true;
233
+ }
234
+ textblocks.push({ node, pos });
235
+ return false;
236
+ });
237
+ return searchTextblocks(textblocks, term, options);
238
+ }
239
+ function findNextIndex(results, from) {
240
+ if (results.length === 0) {
241
+ return null;
242
+ }
243
+ const index = results.findIndex((result) => result.from >= from);
244
+ return index === -1 ? 0 : index;
245
+ }
246
+
247
+ // src/utils/createDecorations.ts
248
+ var import_view2 = require("@tiptap/pm/view");
249
+
250
+ // src/utils/createDecoration.ts
251
+ var import_view = require("@tiptap/pm/view");
252
+ function createDecoration(result, currentIndex, index) {
253
+ const className = index === currentIndex ? `${resultClass} ${currentResultClass}` : resultClass;
254
+ return import_view.Decoration.inline(result.from, result.to, { class: className });
255
+ }
256
+
257
+ // src/utils/createDecorations.ts
258
+ function createDecorations(doc, results, currentIndex) {
259
+ const decorations = results.map((result, index) => createDecoration(result, currentIndex, index));
260
+ return import_view2.DecorationSet.create(doc, decorations);
261
+ }
262
+
263
+ // src/utils/createState.ts
264
+ function createState(doc, options) {
265
+ const results = searchDocument(doc, options.searchTerm, options);
266
+ const currentIndex = results.length > 0 ? 0 : null;
267
+ return {
268
+ searchTerm: options.searchTerm,
269
+ replaceTerm: options.replaceTerm,
270
+ caseSensitive: options.caseSensitive,
271
+ useRegex: options.useRegex,
272
+ wholeWord: options.wholeWord,
273
+ results,
274
+ currentIndex,
275
+ decorations: createDecorations(doc, results, currentIndex)
276
+ };
277
+ }
278
+
279
+ // src/utils/hasCurrentIndex.ts
280
+ function hasCurrentIndex(meta) {
281
+ return !!meta && "currentIndex" in meta;
282
+ }
283
+
284
+ // src/utils/findDecorations.ts
285
+ function findDecorations(decorations, results, textblocks) {
286
+ const found = /* @__PURE__ */ new Set();
287
+ textblocks.forEach((textblock) => {
288
+ decorations.find(textblock.from, textblock.to).forEach((decoration) => found.add(decoration));
289
+ });
290
+ results.forEach((result) => {
291
+ decorations.find(result.from, result.to, (decoration) => {
292
+ return decoration.from === result.from && decoration.to === result.to;
293
+ }).forEach((decoration) => found.add(decoration));
294
+ });
295
+ return [...found];
296
+ }
297
+
298
+ // src/utils/sameResult.ts
299
+ function sameResult(first, second) {
300
+ return first.from === second.from && first.to === second.to;
301
+ }
302
+
303
+ // src/utils/isCurrentResult.ts
304
+ function isCurrentResult(result, current) {
305
+ return current !== null && sameResult(result, current);
306
+ }
307
+
308
+ // src/utils/resultInTextblocks.ts
309
+ function resultInTextblocks(result, textblocks) {
310
+ let start = 0;
311
+ let end = textblocks.length - 1;
312
+ while (start <= end) {
313
+ const index = Math.floor((start + end) / 2);
314
+ const textblock = textblocks[index];
315
+ if (result.from < textblock.from) {
316
+ end = index - 1;
317
+ continue;
318
+ }
319
+ if (result.to > textblock.to) {
320
+ start = index + 1;
321
+ continue;
322
+ }
323
+ return true;
324
+ }
325
+ return false;
326
+ }
327
+
328
+ // src/utils/shouldRefreshResult.ts
329
+ function shouldRefreshResult(result, textblocks, previousCurrent, current) {
330
+ return resultInTextblocks(result, textblocks) || isCurrentResult(result, previousCurrent) || isCurrentResult(result, current);
331
+ }
332
+
333
+ // src/utils/getResultsToRefresh.ts
334
+ function getResultsToRefresh(results, textblocks, previousCurrent, currentIndex) {
335
+ var _a;
336
+ const current = currentIndex === null ? null : (_a = results[currentIndex]) != null ? _a : null;
337
+ const refreshedResults = [];
338
+ results.forEach((result, index) => {
339
+ if (shouldRefreshResult(result, textblocks, previousCurrent, current)) {
340
+ refreshedResults.push({ result, index });
341
+ }
342
+ });
343
+ return refreshedResults;
344
+ }
345
+
346
+ // src/utils/refreshDecorations.ts
347
+ function refreshDecorations(decorations, doc, results, currentIndex, textblocks, previousCurrent) {
348
+ const resultsToRefresh = getResultsToRefresh(results, textblocks, previousCurrent, currentIndex);
349
+ const refreshedResults = resultsToRefresh.map(({ result }) => result);
350
+ const staleResults = previousCurrent ? [...refreshedResults, previousCurrent] : refreshedResults;
351
+ const decorationsToRemove = findDecorations(decorations, staleResults, textblocks);
352
+ const decorationsToAdd = resultsToRefresh.map(({ result, index }) => {
353
+ return createDecoration(result, currentIndex, index);
354
+ });
355
+ return decorations.remove(decorationsToRemove).add(doc, decorationsToAdd);
356
+ }
357
+
358
+ // src/utils/normalizeCurrentIndex.ts
359
+ function normalizeCurrentIndex(currentIndex, length) {
360
+ if (currentIndex === void 0 || currentIndex === null || length === 0) {
361
+ return null;
362
+ }
363
+ return Math.max(0, Math.min(currentIndex, length - 1));
364
+ }
365
+
366
+ // src/utils/getMetaCurrentIndex.ts
367
+ function getMetaCurrentIndex(meta, length) {
368
+ if (!meta) {
369
+ return void 0;
370
+ }
371
+ if (!hasCurrentIndex(meta)) {
372
+ return void 0;
373
+ }
374
+ return normalizeCurrentIndex(meta.currentIndex, length);
375
+ }
376
+
377
+ // src/utils/getFallbackCurrentIndex.ts
378
+ function getFallbackCurrentIndex(previousState, length) {
379
+ return previousState.currentIndex === null ? 0 : Math.min(previousState.currentIndex, length - 1);
380
+ }
381
+
382
+ // src/utils/getPreviousCurrentResult.ts
383
+ function getPreviousCurrentResult(previousState) {
384
+ return previousState.currentIndex === null ? void 0 : previousState.results[previousState.currentIndex];
385
+ }
386
+
387
+ // src/utils/getMappedCurrentIndex.ts
388
+ function getMappedCurrentIndex(previousState, results, docChanged, mapping) {
389
+ if (!docChanged) {
390
+ return void 0;
391
+ }
392
+ if (!mapping) {
393
+ return void 0;
394
+ }
395
+ const previousResult = getPreviousCurrentResult(previousState);
396
+ if (!previousResult) {
397
+ return void 0;
398
+ }
399
+ const mappedFrom = mapping.map(previousResult.from, 1);
400
+ return findNextIndex(results, mappedFrom);
401
+ }
402
+
403
+ // src/utils/isNewSearch.ts
404
+ function isNewSearch(meta) {
405
+ return !!meta && "searchTerm" in meta;
406
+ }
407
+
408
+ // src/utils/getSearchCurrentIndex.ts
409
+ function getSearchCurrentIndex(previousState, results, meta, docChanged, mapping) {
410
+ if (isNewSearch(meta)) {
411
+ return 0;
412
+ }
413
+ const mappedIndex = getMappedCurrentIndex(previousState, results, docChanged, mapping);
414
+ return mappedIndex != null ? mappedIndex : getFallbackCurrentIndex(previousState, results.length);
415
+ }
416
+
417
+ // src/utils/resolveCurrentIndex.ts
418
+ function resolveCurrentIndex(previousState, results, meta, docChanged, mapping) {
419
+ if (results.length === 0) {
420
+ return null;
421
+ }
422
+ const metaIndex = getMetaCurrentIndex(meta, results.length);
423
+ if (metaIndex !== void 0) {
424
+ return metaIndex;
425
+ }
426
+ return getSearchCurrentIndex(previousState, results, meta, docChanged, mapping);
427
+ }
428
+
429
+ // src/utils/refreshCurrentIndexState.ts
430
+ function refreshCurrentIndexState(previousState, state, doc, meta) {
431
+ const currentIndex = resolveCurrentIndex(previousState, state.results, meta, false, void 0);
432
+ const previousCurrent = previousState.currentIndex === null ? null : previousState.results[previousState.currentIndex];
433
+ return {
434
+ ...state,
435
+ currentIndex,
436
+ decorations: refreshDecorations(
437
+ previousState.decorations,
438
+ doc,
439
+ state.results,
440
+ currentIndex,
441
+ [],
442
+ previousCurrent
443
+ )
444
+ };
445
+ }
446
+
447
+ // src/utils/getChangedTextblocks.ts
448
+ function getChangedTextblocks(doc, mapping) {
449
+ const textblocks = /* @__PURE__ */ new Map();
450
+ mapping.maps.forEach((stepMap, index) => {
451
+ const remaining = mapping.slice(index + 1);
452
+ stepMap.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
453
+ const from = Math.max(0, remaining.map(newStart, -1) - 1);
454
+ const to = Math.min(doc.content.size, remaining.map(newEnd, 1) + 1);
455
+ doc.nodesBetween(from, to, (node, pos) => {
456
+ if (!node.isTextblock) {
457
+ return true;
458
+ }
459
+ textblocks.set(pos, { node, pos, from: pos, to: pos + node.nodeSize });
460
+ return false;
461
+ });
462
+ });
463
+ });
464
+ return [...textblocks.values()].sort((first, second) => first.pos - second.pos);
465
+ }
466
+
467
+ // src/utils/mapResult.ts
468
+ function mapResult(result, mapping) {
469
+ const from = mapping.map(result.from, 1);
470
+ const to = mapping.map(result.to, -1);
471
+ return from < to ? { from, to } : null;
472
+ }
473
+
474
+ // src/utils/mapResults.ts
475
+ function mapResults(results, mapping) {
476
+ const mappedResults = [];
477
+ results.forEach((result) => {
478
+ const mapped = mapResult(result, mapping);
479
+ if (mapped) {
480
+ mappedResults.push(mapped);
481
+ }
482
+ });
483
+ return mappedResults;
484
+ }
485
+
486
+ // src/utils/mergeResults.ts
487
+ function mergeResults(first, second) {
488
+ const results = [];
489
+ let firstIndex = 0;
490
+ let secondIndex = 0;
491
+ while (firstIndex < first.length && secondIndex < second.length) {
492
+ if (first[firstIndex].from < second[secondIndex].from) {
493
+ results.push(first[firstIndex++]);
494
+ continue;
495
+ }
496
+ results.push(second[secondIndex++]);
497
+ }
498
+ return results.concat(first.slice(firstIndex), second.slice(secondIndex));
499
+ }
500
+
501
+ // src/utils/refreshDocumentState.ts
502
+ function refreshDocumentState(previousState, state, doc, meta, mapping) {
503
+ const textblocks = getChangedTextblocks(doc, mapping);
504
+ const mappedResults = mapResults(previousState.results, mapping);
505
+ const unaffectedResults = mappedResults.filter((result) => !resultInTextblocks(result, textblocks));
506
+ const changedResults = searchTextblocks(textblocks, state.searchTerm, state);
507
+ const results = mergeResults(unaffectedResults, changedResults);
508
+ const currentIndex = resolveCurrentIndex(previousState, results, meta, true, mapping);
509
+ const previousCurrent = previousState.currentIndex === null ? null : mapResult(previousState.results[previousState.currentIndex], mapping);
510
+ const decorations = previousState.decorations.map(mapping, doc);
511
+ return {
512
+ ...state,
513
+ results,
514
+ currentIndex,
515
+ decorations: refreshDecorations(
516
+ decorations,
517
+ doc,
518
+ results,
519
+ currentIndex,
520
+ textblocks,
521
+ previousCurrent
522
+ )
523
+ };
524
+ }
525
+
526
+ // src/utils/refreshState.ts
527
+ function refreshState(previousState, state, doc, meta, docChanged, mapping) {
528
+ const results = searchDocument(doc, state.searchTerm, state);
529
+ const currentIndex = resolveCurrentIndex(previousState, results, meta, docChanged, mapping);
530
+ return {
531
+ ...state,
532
+ results,
533
+ currentIndex,
534
+ decorations: createDecorations(doc, results, currentIndex)
535
+ };
536
+ }
537
+
538
+ // src/utils/touchesSearch.ts
539
+ var searchKeys = ["searchTerm", "caseSensitive", "useRegex", "wholeWord"];
540
+ function touchesSearch(meta) {
541
+ return !!meta && searchKeys.some((key) => key in meta);
542
+ }
543
+
544
+ // src/utils/updateState.ts
545
+ function updateState(previousState, state, doc, meta, docChanged, mapping) {
546
+ if (touchesSearch(meta)) {
547
+ return refreshState(previousState, state, doc, meta, docChanged, mapping);
548
+ }
549
+ if (docChanged) {
550
+ return refreshDocumentState(previousState, state, doc, meta, mapping);
551
+ }
552
+ if (hasCurrentIndex(meta)) {
553
+ return refreshCurrentIndexState(previousState, state, doc, meta);
554
+ }
555
+ return state;
556
+ }
557
+
558
+ // src/plugin/plugin.ts
559
+ var FindAndReplacePluginKey = new import_state.PluginKey("findAndReplace");
560
+ var FindAndReplacePlugin = (options, onCreate) => {
561
+ return new import_state.Plugin({
562
+ key: FindAndReplacePluginKey,
563
+ state: {
564
+ init: (_config, state) => {
565
+ if (options.injectCSS && typeof document !== "undefined") {
566
+ createStyleTag(style, options.injectNonce);
567
+ }
568
+ const pluginState = createState(state.doc, options);
569
+ onCreate(pluginState);
570
+ return pluginState;
571
+ },
572
+ apply: (tr, state, _oldState, newState) => {
573
+ const meta = tr.getMeta(FindAndReplacePluginKey);
574
+ if (!meta && !tr.docChanged) {
575
+ return state;
576
+ }
577
+ return updateState(
578
+ state,
579
+ { ...state, ...meta },
580
+ newState.doc,
581
+ meta,
582
+ tr.docChanged,
583
+ tr.mapping
584
+ );
585
+ }
586
+ },
587
+ props: {
588
+ decorations(state) {
589
+ var _a;
590
+ return (_a = FindAndReplacePluginKey.getState(state)) == null ? void 0 : _a.decorations;
591
+ }
592
+ }
593
+ });
594
+ };
595
+
596
+ // src/utils/getTextNodeResult.ts
597
+ function getTextNodeResult(state, result) {
598
+ const $from = state.doc.resolve(result.from);
599
+ const node = $from.parent.child($from.index());
600
+ if (!node.isText || !node.text) {
601
+ return null;
602
+ }
603
+ const from = result.from - $from.textOffset;
604
+ return result.to <= from + node.nodeSize ? { from, text: node.text } : null;
605
+ }
606
+
607
+ // src/utils/groupResults.ts
608
+ function shouldMergeResult(group, textNode) {
609
+ if (!group || !textNode) {
610
+ return false;
611
+ }
612
+ return group.from === textNode.from && group.text !== null;
613
+ }
614
+ function createTextResultGroup(result, textNode) {
615
+ return {
616
+ from: textNode.from,
617
+ text: textNode.text,
618
+ results: [result]
619
+ };
620
+ }
621
+ function createResultGroup(result) {
622
+ return {
623
+ from: result.from,
624
+ text: null,
625
+ results: [result]
626
+ };
627
+ }
628
+ function appendResult(groups, result, textNode) {
629
+ const group = groups.at(-1);
630
+ if (shouldMergeResult(group, textNode)) {
631
+ group.results.push(result);
632
+ return;
633
+ }
634
+ if (textNode) {
635
+ groups.push(createTextResultGroup(result, textNode));
636
+ return;
637
+ }
638
+ groups.push(createResultGroup(result));
639
+ }
640
+ function groupResults(state, results) {
641
+ const groups = [];
642
+ for (const result of results) {
643
+ appendResult(groups, result, getTextNodeResult(state, result));
644
+ }
645
+ return groups;
646
+ }
647
+
648
+ // src/utils/replaceGroup.ts
649
+ function replaceSeparateResults(tr, results, replaceTerm) {
650
+ for (const result of [...results].reverse()) {
651
+ tr.insertText(replaceTerm, result.from, result.to);
652
+ }
653
+ }
654
+ function createReplacement(group, text, firstResult, replaceTerm) {
655
+ let offset = firstResult.from - group.from;
656
+ const replacement = [];
657
+ for (const result of group.results) {
658
+ const from = result.from - group.from;
659
+ const to = result.to - group.from;
660
+ replacement.push(text.slice(offset, from), replaceTerm);
661
+ offset = to;
662
+ }
663
+ return replacement.join("");
664
+ }
665
+ function replaceGroup(tr, group, replaceTerm) {
666
+ const firstResult = group.results[0];
667
+ const lastResult = group.results.at(-1);
668
+ if (!lastResult) {
669
+ return;
670
+ }
671
+ if (group.text === null) {
672
+ replaceSeparateResults(tr, group.results, replaceTerm);
673
+ return;
674
+ }
675
+ tr.insertText(
676
+ createReplacement(group, group.text, firstResult, replaceTerm),
677
+ firstResult.from,
678
+ lastResult.to
679
+ );
680
+ }
681
+
682
+ // src/utils/replaceAllResults.ts
683
+ function replaceAllResults(tr, state, results, replaceTerm) {
684
+ const groups = groupResults(state, results);
685
+ for (const group of groups.reverse()) {
686
+ replaceGroup(tr, group, replaceTerm);
687
+ }
688
+ }
689
+
690
+ // src/find-and-replace.ts
691
+ function mergePluginMeta(tr, meta) {
692
+ const currentMeta = tr.getMeta(FindAndReplacePluginKey);
693
+ tr.setMeta(FindAndReplacePluginKey, { ...currentMeta, ...meta });
694
+ }
695
+ function setPluginMeta(meta) {
696
+ return ({ tr, dispatch }) => {
697
+ if (dispatch) {
698
+ mergePluginMeta(tr, meta);
699
+ }
700
+ return true;
701
+ };
702
+ }
703
+ function applySearchTerm(editor, term) {
704
+ const tr = editor.state.tr;
705
+ mergePluginMeta(tr, { searchTerm: term });
706
+ editor.view.dispatch(tr);
707
+ }
708
+ function consumePendingSearch(editor) {
709
+ const state = getDebouncedSearchState(editor);
710
+ if (state.pendingTerm === null) {
711
+ return null;
712
+ }
713
+ const pendingTerm = state.pendingTerm;
714
+ state.pendingTerm = null;
715
+ if (state.timeout !== null) {
716
+ clearTimeout(state.timeout);
717
+ state.timeout = null;
718
+ }
719
+ return pendingTerm;
720
+ }
721
+ function getPendingAwarePluginState(state, pendingTerm) {
722
+ const pluginState = FindAndReplacePluginKey.getState(state);
723
+ if (!pluginState) {
724
+ return null;
725
+ }
726
+ if (pendingTerm === null) {
727
+ return pluginState.results.length === 0 ? null : pluginState;
728
+ }
729
+ const results = searchDocument(state.doc, pendingTerm, {
730
+ caseSensitive: pluginState.caseSensitive,
731
+ useRegex: pluginState.useRegex,
732
+ wholeWord: pluginState.wholeWord
733
+ });
734
+ if (results.length === 0) {
735
+ return null;
736
+ }
737
+ return {
738
+ ...pluginState,
739
+ searchTerm: pendingTerm,
740
+ results,
741
+ currentIndex: 0
742
+ };
743
+ }
744
+ function currentResult(pluginState) {
745
+ var _a;
746
+ return pluginState.results[(_a = pluginState.currentIndex) != null ? _a : 0];
747
+ }
748
+ function shiftIndex(currentIndex, length, direction) {
749
+ if (currentIndex === null) {
750
+ return direction === 1 ? 0 : length - 1;
751
+ }
752
+ return (currentIndex + direction + length) % length;
753
+ }
754
+ function selectResult(tr, result, index) {
755
+ tr.setSelection(import_state2.TextSelection.create(tr.doc, result.from, result.to));
756
+ mergePluginMeta(tr, { currentIndex: index });
757
+ tr.scrollIntoView();
758
+ }
759
+ function replaceResult(tr, pluginState, result) {
760
+ const { searchTerm, replaceTerm, caseSensitive, useRegex, wholeWord } = pluginState;
761
+ tr.insertText(replaceTerm, result.from, result.to);
762
+ const results = searchDocument(tr.doc, searchTerm, { caseSensitive, useRegex, wholeWord });
763
+ const nextIndex = findNextIndex(results, result.from + replaceTerm.length);
764
+ if (nextIndex === null) {
765
+ mergePluginMeta(tr, { currentIndex: null });
766
+ return;
767
+ }
768
+ selectResult(tr, results[nextIndex], nextIndex);
769
+ }
770
+ var FindAndReplace = import_core.Extension.create({
771
+ name: "findAndReplace",
772
+ addOptions() {
773
+ return {
774
+ searchTerm: "",
775
+ replaceTerm: "",
776
+ caseSensitive: false,
777
+ useRegex: false,
778
+ wholeWord: false,
779
+ searchDebounceMs: 250,
780
+ injectCSS: true,
781
+ injectNonce: void 0
782
+ };
783
+ },
784
+ addStorage() {
785
+ return {
786
+ searchTerm: this.options.searchTerm,
787
+ replaceTerm: this.options.replaceTerm,
788
+ caseSensitive: this.options.caseSensitive,
789
+ useRegex: this.options.useRegex,
790
+ wholeWord: this.options.wholeWord,
791
+ results: [],
792
+ currentIndex: null
793
+ };
794
+ },
795
+ addProseMirrorPlugins() {
796
+ return [
797
+ FindAndReplacePlugin(this.options, (pluginState) => {
798
+ this.storage.results = pluginState.results;
799
+ this.storage.currentIndex = pluginState.currentIndex;
800
+ })
801
+ ];
802
+ },
803
+ onTransaction() {
804
+ const pluginState = FindAndReplacePluginKey.getState(this.editor.state);
805
+ if (!pluginState) {
806
+ return;
807
+ }
808
+ this.storage.searchTerm = pluginState.searchTerm;
809
+ this.storage.replaceTerm = pluginState.replaceTerm;
810
+ this.storage.caseSensitive = pluginState.caseSensitive;
811
+ this.storage.useRegex = pluginState.useRegex;
812
+ this.storage.wholeWord = pluginState.wholeWord;
813
+ this.storage.results = pluginState.results;
814
+ this.storage.currentIndex = pluginState.currentIndex;
815
+ },
816
+ addCommands() {
817
+ const scheduleSearchTerm = (term) => {
818
+ const state = getDebouncedSearchState(this.editor);
819
+ if (this.options.searchDebounceMs <= 0) {
820
+ if (state.timeout !== null) {
821
+ clearTimeout(state.timeout);
822
+ state.timeout = null;
823
+ }
824
+ state.pendingTerm = null;
825
+ applySearchTerm(this.editor, term);
826
+ return true;
827
+ }
828
+ if (state.timeout !== null) {
829
+ clearTimeout(state.timeout);
830
+ }
831
+ state.pendingTerm = term;
832
+ state.timeout = setTimeout(() => {
833
+ const debouncedState = getDebouncedSearchState(this.editor);
834
+ debouncedState.timeout = null;
835
+ if (debouncedState.pendingTerm === null) {
836
+ return;
837
+ }
838
+ const pendingTerm = debouncedState.pendingTerm;
839
+ debouncedState.pendingTerm = null;
840
+ applySearchTerm(this.editor, pendingTerm);
841
+ }, this.options.searchDebounceMs);
842
+ return true;
843
+ };
844
+ return {
845
+ setSearchTerm: (term) => ({ dispatch }) => {
846
+ if (!dispatch) {
847
+ return true;
848
+ }
849
+ return scheduleSearchTerm(term);
850
+ },
851
+ setReplaceTerm: (term) => setPluginMeta({ replaceTerm: term }),
852
+ setCaseSensitive: (caseSensitive) => setPluginMeta({ caseSensitive }),
853
+ setUseRegex: (useRegex) => setPluginMeta({ useRegex }),
854
+ setWholeWord: (wholeWord) => setPluginMeta({ wholeWord }),
855
+ clearSearch: () => ({ tr, dispatch }) => {
856
+ const state = getDebouncedSearchState(this.editor);
857
+ if (state.timeout !== null) {
858
+ clearTimeout(state.timeout);
859
+ state.timeout = null;
860
+ }
861
+ state.pendingTerm = null;
862
+ if (dispatch) {
863
+ mergePluginMeta(tr, { searchTerm: "" });
864
+ }
865
+ return true;
866
+ },
867
+ replace: () => ({ tr, state, dispatch }) => {
868
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null;
869
+ if (dispatch && pendingTerm !== null) {
870
+ mergePluginMeta(tr, { searchTerm: pendingTerm });
871
+ }
872
+ const pluginState = getPendingAwarePluginState(state, pendingTerm);
873
+ if (!pluginState) {
874
+ return false;
875
+ }
876
+ const result = currentResult(pluginState);
877
+ if (!result) {
878
+ return false;
879
+ }
880
+ if (dispatch) {
881
+ replaceResult(tr, pluginState, result);
882
+ }
883
+ return true;
884
+ },
885
+ replaceAll: () => ({ tr, state, dispatch }) => {
886
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null;
887
+ if (dispatch && pendingTerm !== null) {
888
+ mergePluginMeta(tr, { searchTerm: pendingTerm });
889
+ }
890
+ const pluginState = getPendingAwarePluginState(state, pendingTerm);
891
+ if (!pluginState) {
892
+ return false;
893
+ }
894
+ if (dispatch) {
895
+ replaceAllResults(tr, state, pluginState.results, pluginState.replaceTerm);
896
+ }
897
+ return true;
898
+ },
899
+ goToNextResult: () => ({ tr, state, dispatch }) => {
900
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null;
901
+ if (dispatch && pendingTerm !== null) {
902
+ mergePluginMeta(tr, { searchTerm: pendingTerm });
903
+ }
904
+ const pluginState = getPendingAwarePluginState(state, pendingTerm);
905
+ if (!pluginState) {
906
+ return false;
907
+ }
908
+ const nextIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, 1);
909
+ if (dispatch) {
910
+ selectResult(tr, pluginState.results[nextIndex], nextIndex);
911
+ }
912
+ return true;
913
+ },
914
+ goToPreviousResult: () => ({ tr, state, dispatch }) => {
915
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null;
916
+ if (dispatch && pendingTerm !== null) {
917
+ mergePluginMeta(tr, { searchTerm: pendingTerm });
918
+ }
919
+ const pluginState = getPendingAwarePluginState(state, pendingTerm);
920
+ if (!pluginState) {
921
+ return false;
922
+ }
923
+ const previousIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, -1);
924
+ if (dispatch) {
925
+ selectResult(tr, pluginState.results[previousIndex], previousIndex);
926
+ }
927
+ return true;
928
+ }
929
+ };
930
+ },
931
+ onDestroy() {
932
+ clearDebouncedSearchState(this.editor);
933
+ }
934
+ });
935
+
936
+ // src/index.ts
937
+ var index_default = FindAndReplace;
938
+ // Annotate the CommonJS export names for ESM import in node:
939
+ 0 && (module.exports = {
940
+ FindAndReplace,
941
+ FindAndReplacePlugin,
942
+ FindAndReplacePluginKey,
943
+ createSearchRegex,
944
+ currentResultClass,
945
+ findNextIndex,
946
+ resultClass,
947
+ searchDocument,
948
+ searchTextblocks
949
+ });
950
+ //# sourceMappingURL=index.cjs.map