@sereneinserenade/tiptap-search-and-replace
Advanced tools
Comparing version 0.0.3 to 0.0.4
@@ -18,3 +18,3 @@ 'use strict'; | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// cepies or substantial portions of the Software. | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
@@ -28,3 +28,3 @@ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
const getRegex = (s, disableRegex, caseSensitive) => { | ||
return RegExp(disableRegex ? s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") : s, caseSensitive ? "gu" : "gui"); | ||
return RegExp(disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s, caseSensitive ? "gu" : "gui"); | ||
}; | ||
@@ -95,3 +95,3 @@ function processSearches(doc, searchTerm, searchResultClass) { | ||
const { from: currentFrom, to: currentTo } = results[index]; | ||
const offset = (currentTo - currentFrom - replaceTerm.length) + lastOffset; | ||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset; | ||
const { from, to } = results[nextIndex]; | ||
@@ -163,3 +163,3 @@ results[nextIndex] = { | ||
const { searchResultClass, disableRegex, caseSensitive } = this.options; | ||
const setLastSearchTerm = (t) => editor.storage.searchAndReplace.lastSearchTerm = t; | ||
const setLastSearchTerm = (t) => (editor.storage.searchAndReplace.lastSearchTerm = t); | ||
return [ | ||
@@ -177,3 +177,3 @@ new state.Plugin({ | ||
return view.DecorationSet.empty; | ||
const { decorationsToReturn, results, } = processSearches(doc, getRegex(searchTerm, disableRegex, caseSensitive), searchResultClass); | ||
const { decorationsToReturn, results } = processSearches(doc, getRegex(searchTerm, disableRegex, caseSensitive), searchResultClass); | ||
editor.storage.searchAndReplace.results = results; | ||
@@ -180,0 +180,0 @@ return decorationsToReturn; |
@@ -14,3 +14,3 @@ import { Extension } from '@tiptap/core'; | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// cepies or substantial portions of the Software. | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
@@ -24,3 +24,3 @@ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
const getRegex = (s, disableRegex, caseSensitive) => { | ||
return RegExp(disableRegex ? s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") : s, caseSensitive ? "gu" : "gui"); | ||
return RegExp(disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s, caseSensitive ? "gu" : "gui"); | ||
}; | ||
@@ -91,3 +91,3 @@ function processSearches(doc, searchTerm, searchResultClass) { | ||
const { from: currentFrom, to: currentTo } = results[index]; | ||
const offset = (currentTo - currentFrom - replaceTerm.length) + lastOffset; | ||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset; | ||
const { from, to } = results[nextIndex]; | ||
@@ -159,3 +159,3 @@ results[nextIndex] = { | ||
const { searchResultClass, disableRegex, caseSensitive } = this.options; | ||
const setLastSearchTerm = (t) => editor.storage.searchAndReplace.lastSearchTerm = t; | ||
const setLastSearchTerm = (t) => (editor.storage.searchAndReplace.lastSearchTerm = t); | ||
return [ | ||
@@ -173,3 +173,3 @@ new Plugin({ | ||
return DecorationSet.empty; | ||
const { decorationsToReturn, results, } = processSearches(doc, getRegex(searchTerm, disableRegex, caseSensitive), searchResultClass); | ||
const { decorationsToReturn, results } = processSearches(doc, getRegex(searchTerm, disableRegex, caseSensitive), searchResultClass); | ||
editor.storage.searchAndReplace.results = results; | ||
@@ -176,0 +176,0 @@ return decorationsToReturn; |
{ | ||
"name": "@sereneinserenade/tiptap-search-and-replace", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "Tiptap Extension for adding comments", | ||
@@ -42,5 +42,5 @@ "keywords": [ | ||
"peerDependencies": { | ||
"@tiptap/core": "^2.1.8", | ||
"@tiptap/pm": "^2.1.8" | ||
"@tiptap/core": "^2.x.x", | ||
"@tiptap/pm": "^2.x.x" | ||
} | ||
} |
@@ -23,6 +23,6 @@ // MIT License | ||
import { Extension, Range } from "@tiptap/core" | ||
import { Decoration, DecorationSet } from "@tiptap/pm/view" | ||
import { Plugin, PluginKey } from "@tiptap/pm/state" | ||
import { Node as PMNode } from "@tiptap/pm/model" | ||
import { Extension, Range } from "@tiptap/core"; | ||
import { Decoration, DecorationSet } from "@tiptap/pm/view"; | ||
import { Plugin, PluginKey } from "@tiptap/pm/state"; | ||
import { Node as PMNode } from "@tiptap/pm/model"; | ||
@@ -35,16 +35,16 @@ declare module "@tiptap/core" { | ||
*/ | ||
setSearchTerm: (searchTerm: string) => ReturnType, | ||
setSearchTerm: (searchTerm: string) => ReturnType; | ||
/** | ||
* @description Set replace term in extension. | ||
*/ | ||
setReplaceTerm: (replaceTerm: string) => ReturnType, | ||
setReplaceTerm: (replaceTerm: string) => ReturnType; | ||
/** | ||
* @description Replace first instance of search result with given replace term. | ||
*/ | ||
replace: () => ReturnType, | ||
replace: () => ReturnType; | ||
/** | ||
* @description Replace all instances of search result with given replace term. | ||
*/ | ||
replaceAll: () => ReturnType, | ||
} | ||
replaceAll: () => ReturnType; | ||
}; | ||
} | ||
@@ -58,19 +58,31 @@ } | ||
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => { | ||
return RegExp(disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s, caseSensitive ? "gu" : "gui"); | ||
} | ||
const getRegex = ( | ||
s: string, | ||
disableRegex: boolean, | ||
caseSensitive: boolean, | ||
): RegExp => { | ||
return RegExp( | ||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s, | ||
caseSensitive ? "gu" : "gui", | ||
); | ||
}; | ||
interface ProcessedSearches { | ||
decorationsToReturn: DecorationSet, | ||
results: Range[] | ||
decorationsToReturn: DecorationSet; | ||
results: Range[]; | ||
} | ||
function processSearches (doc: PMNode, searchTerm: RegExp, searchResultClass: string): ProcessedSearches { | ||
const decorations: Decoration[] = [] | ||
let textNodesWithPosition: TextNodesWithPosition[] = [] | ||
const results: Range[] = [] | ||
function processSearches( | ||
doc: PMNode, | ||
searchTerm: RegExp, | ||
searchResultClass: string, | ||
): ProcessedSearches { | ||
const decorations: Decoration[] = []; | ||
let textNodesWithPosition: TextNodesWithPosition[] = []; | ||
const results: Range[] = []; | ||
let index = 0 | ||
let index = 0; | ||
if (!searchTerm) return { decorationsToReturn: DecorationSet.empty, results: [] } | ||
if (!searchTerm) | ||
return { decorationsToReturn: DecorationSet.empty, results: [] }; | ||
@@ -83,3 +95,3 @@ doc?.descendants((node, pos) => { | ||
pos: textNodesWithPosition[index].pos, | ||
} | ||
}; | ||
} else { | ||
@@ -89,20 +101,22 @@ textNodesWithPosition[index] = { | ||
pos, | ||
} | ||
}; | ||
} | ||
} else { | ||
index += 1 | ||
index += 1; | ||
} | ||
}) | ||
}); | ||
textNodesWithPosition = textNodesWithPosition.filter(Boolean) | ||
textNodesWithPosition = textNodesWithPosition.filter(Boolean); | ||
for (let i = 0; i < textNodesWithPosition.length; i += 1) { | ||
const { text, pos } = textNodesWithPosition[i] | ||
const { text, pos } = textNodesWithPosition[i]; | ||
const matches = Array.from(text.matchAll(searchTerm)).filter(([ matchText ]) => matchText.trim()) | ||
const matches = Array.from(text.matchAll(searchTerm)).filter( | ||
([matchText]) => matchText.trim(), | ||
); | ||
for (let j = 0; j < matches.length; j += 1) { | ||
const m = matches[j] | ||
const m = matches[j]; | ||
if (m[0] === "") break | ||
if (m[0] === "") break; | ||
@@ -113,3 +127,3 @@ if (m.index !== undefined) { | ||
to: pos + m.index + m[0].length, | ||
}) | ||
}); | ||
} | ||
@@ -120,4 +134,6 @@ } | ||
for (let i = 0; i < results.length; i += 1) { | ||
const r = results[i] | ||
decorations.push(Decoration.inline(r.from, r.to, { class: searchResultClass })) | ||
const r = results[i]; | ||
decorations.push( | ||
Decoration.inline(r.from, r.to, { class: searchResultClass }), | ||
); | ||
} | ||
@@ -128,25 +144,34 @@ | ||
results, | ||
} | ||
}; | ||
} | ||
const replace = (replaceTerm: string, results: Range[], { state, dispatch }: any) => { | ||
const firstResult = results[0] | ||
const replace = ( | ||
replaceTerm: string, | ||
results: Range[], | ||
{ state, dispatch }: any, | ||
) => { | ||
const firstResult = results[0]; | ||
if (!firstResult) return | ||
if (!firstResult) return; | ||
const { from, to } = results[0] | ||
const { from, to } = results[0]; | ||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to)) | ||
} | ||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to)); | ||
}; | ||
const rebaseNextResult = (replaceTerm: string, index: number, lastOffset: number, results: Range[]): [number, Range[]] | null => { | ||
const nextIndex = index + 1 | ||
const rebaseNextResult = ( | ||
replaceTerm: string, | ||
index: number, | ||
lastOffset: number, | ||
results: Range[], | ||
): [number, Range[]] | null => { | ||
const nextIndex = index + 1; | ||
if (!results[nextIndex]) return null | ||
if (!results[nextIndex]) return null; | ||
const { from: currentFrom, to: currentTo } = results[index] | ||
const { from: currentFrom, to: currentTo } = results[index]; | ||
const offset = (currentTo - currentFrom - replaceTerm.length) + lastOffset | ||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset; | ||
const { from, to } = results[nextIndex] | ||
const { from, to } = results[nextIndex]; | ||
@@ -156,31 +181,42 @@ results[nextIndex] = { | ||
from: from - offset, | ||
} | ||
}; | ||
return [ offset, results ] | ||
} | ||
return [offset, results]; | ||
}; | ||
const replaceAll = (replaceTerm: string, results: Range[], { tr, dispatch }: any) => { | ||
let offset = 0 | ||
const replaceAll = ( | ||
replaceTerm: string, | ||
results: Range[], | ||
{ tr, dispatch }: any, | ||
) => { | ||
let offset = 0; | ||
let resultsCopy = results.slice() | ||
let resultsCopy = results.slice(); | ||
if (!resultsCopy.length) return | ||
if (!resultsCopy.length) return; | ||
for (let i = 0; i < resultsCopy.length; i += 1) { | ||
const { from, to } = resultsCopy[i] | ||
const { from, to } = resultsCopy[i]; | ||
tr.insertText(replaceTerm, from, to) | ||
tr.insertText(replaceTerm, from, to); | ||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy) | ||
const rebaseNextResultResponse = rebaseNextResult( | ||
replaceTerm, | ||
i, | ||
offset, | ||
resultsCopy, | ||
); | ||
if (!rebaseNextResultResponse) continue | ||
if (!rebaseNextResultResponse) continue; | ||
offset = rebaseNextResultResponse[0] | ||
resultsCopy = rebaseNextResultResponse[1] | ||
offset = rebaseNextResultResponse[0]; | ||
resultsCopy = rebaseNextResultResponse[1]; | ||
} | ||
dispatch(tr) | ||
} | ||
dispatch(tr); | ||
}; | ||
export const searchAndReplacePluginKey = new PluginKey("searchAndReplacePlugin") | ||
export const searchAndReplacePluginKey = new PluginKey( | ||
"searchAndReplacePlugin", | ||
); | ||
@@ -200,6 +236,9 @@ export interface SearchAndReplaceOptions { | ||
export const SearchAndReplace = Extension.create<SearchAndReplaceOptions, SearchAndReplaceStorage>({ | ||
export const SearchAndReplace = Extension.create< | ||
SearchAndReplaceOptions, | ||
SearchAndReplaceStorage | ||
>({ | ||
name: "searchAndReplace", | ||
addOptions () { | ||
addOptions() { | ||
return { | ||
@@ -209,6 +248,6 @@ searchResultClass: "search-result", | ||
disableRegex: true, | ||
} | ||
}; | ||
}, | ||
addStorage () { | ||
addStorage() { | ||
return { | ||
@@ -219,39 +258,48 @@ searchTerm: "", | ||
lastSearchTerm: "", | ||
} | ||
}; | ||
}, | ||
addCommands () { | ||
addCommands() { | ||
return { | ||
setSearchTerm: (searchTerm: string) => ({ editor }) => { | ||
editor.storage.searchAndReplace.searchTerm = searchTerm | ||
setSearchTerm: | ||
(searchTerm: string) => | ||
({ editor }) => { | ||
editor.storage.searchAndReplace.searchTerm = searchTerm; | ||
return false | ||
}, | ||
setReplaceTerm: (replaceTerm: string) => ({ editor }) => { | ||
editor.storage.searchAndReplace.replaceTerm = replaceTerm | ||
return false; | ||
}, | ||
setReplaceTerm: | ||
(replaceTerm: string) => | ||
({ editor }) => { | ||
editor.storage.searchAndReplace.replaceTerm = replaceTerm; | ||
return false | ||
}, | ||
replace: () => ({ editor, state, dispatch }) => { | ||
const { replaceTerm, results } = editor.storage.searchAndReplace | ||
return false; | ||
}, | ||
replace: | ||
() => | ||
({ editor, state, dispatch }) => { | ||
const { replaceTerm, results } = editor.storage.searchAndReplace; | ||
replace(replaceTerm, results, { state, dispatch }) | ||
replace(replaceTerm, results, { state, dispatch }); | ||
return false | ||
}, | ||
replaceAll: () => ({ editor, tr, dispatch }) => { | ||
const { replaceTerm, results } = editor.storage.searchAndReplace | ||
return false; | ||
}, | ||
replaceAll: | ||
() => | ||
({ editor, tr, dispatch }) => { | ||
const { replaceTerm, results } = editor.storage.searchAndReplace; | ||
replaceAll(replaceTerm, results, { tr, dispatch }) | ||
replaceAll(replaceTerm, results, { tr, dispatch }); | ||
return false | ||
}, | ||
} | ||
return false; | ||
}, | ||
}; | ||
}, | ||
addProseMirrorPlugins () { | ||
const editor = this.editor | ||
const { searchResultClass, disableRegex, caseSensitive } = this.options | ||
addProseMirrorPlugins() { | ||
const editor = this.editor; | ||
const { searchResultClass, disableRegex, caseSensitive } = this.options; | ||
const setLastSearchTerm = (t) => editor.storage.searchAndReplace.lastSearchTerm = t | ||
const setLastSearchTerm = (t: string) => | ||
(editor.storage.searchAndReplace.lastSearchTerm = t); | ||
@@ -263,31 +311,33 @@ return [ | ||
init: () => DecorationSet.empty, | ||
apply ({ doc, docChanged }, oldState) { | ||
const { searchTerm, lastSearchTerm } = editor.storage.searchAndReplace | ||
apply({ doc, docChanged }, oldState) { | ||
const { searchTerm, lastSearchTerm } = | ||
editor.storage.searchAndReplace; | ||
if (!docChanged && lastSearchTerm === searchTerm) return oldState | ||
if (!docChanged && lastSearchTerm === searchTerm) return oldState; | ||
setLastSearchTerm(searchTerm) | ||
setLastSearchTerm(searchTerm); | ||
if (!searchTerm) return DecorationSet.empty | ||
if (!searchTerm) return DecorationSet.empty; | ||
const { | ||
decorationsToReturn, | ||
results, | ||
} = processSearches(doc, getRegex(searchTerm, disableRegex, caseSensitive), searchResultClass) | ||
const { decorationsToReturn, results } = processSearches( | ||
doc, | ||
getRegex(searchTerm, disableRegex, caseSensitive), | ||
searchResultClass, | ||
); | ||
editor.storage.searchAndReplace.results = results | ||
editor.storage.searchAndReplace.results = results; | ||
return decorationsToReturn | ||
return decorationsToReturn; | ||
}, | ||
}, | ||
props: { | ||
decorations (state) { | ||
return this.getState(state) | ||
decorations(state) { | ||
return this.getState(state); | ||
}, | ||
}, | ||
}), | ||
] | ||
]; | ||
}, | ||
}) | ||
}); | ||
export default SearchAndReplace | ||
export default SearchAndReplace; |
35295
725