123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- // Modified version based on 'highlight-words-core'
- import { isString } from 'lodash-es';
- const escapeRegExpFn = (string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
- interface ChunkQuery {
- autoEscape?: boolean;
- caseSensitive?: boolean;
- searchWords: string[];
- sourceString: string;
- }
- interface Chunk {
- start: number;
- end: number;
- highlight: boolean;
- }
- /**
- * Examine text for any matches.
- * If we find matches, add them to the returned array as a "chunk" object ({start:number, end:number}).
- * @return { start:number, end:number }[]
- */
- const findChunks = ({
- autoEscape,
- caseSensitive,
- searchWords,
- sourceString
- }: ChunkQuery): Chunk[] => (
- searchWords
- .filter(searchWord => searchWord) // Remove empty words
- .reduce((chunks, searchWord) => {
- if (autoEscape) {
- searchWord = escapeRegExpFn(searchWord);
- }
- const regex = new RegExp(searchWord, caseSensitive ? 'g' : 'gi');
- let match;
- while ((match = regex.exec(sourceString))) {
- const start = match.index;
- const end = regex.lastIndex;
- // We do not return zero-length matches
- if (end > start) {
- chunks.push({ highlight: false, start, end });
- }
- // Prevent browsers like Firefox from getting stuck in an infinite loop
- // See http://www.regexguru.com/2008/04/watch-out-for-zero-length-matches/
- if (match.index === regex.lastIndex) {
- regex.lastIndex++;
- }
- }
- return chunks;
- }, [])
- );
- /**
- * Takes an array of {start:number, end:number} objects and combines chunks that overlap into single chunks.
- * @return {start:number, end:number}[]
- */
- const combineChunks = ({ chunks }: { chunks: Chunk[] }) => {
- chunks = chunks
- .sort((first, second) => first.start - second.start)
- .reduce((processedChunks, nextChunk) => {
- // First chunk just goes straight in the array...
- if (processedChunks.length === 0) {
- return [nextChunk];
- } else {
- // ... subsequent chunks get checked to see if they overlap...
- const prevChunk = processedChunks.pop();
- if (nextChunk.start <= prevChunk.end) {
- // It may be the case that prevChunk completely surrounds nextChunk, so take the
- // largest of the end indeces.
- const endIndex = Math.max(prevChunk.end, nextChunk.end);
- processedChunks.push({
- highlight: false,
- start: prevChunk.start,
- end: endIndex
- });
- } else {
- processedChunks.push(prevChunk, nextChunk);
- }
- return processedChunks;
- }
- }, []);
- return chunks;
- };
- /**
- * Given a set of chunks to highlight, create an additional set of chunks
- * to represent the bits of text between the highlighted text.
- * @param chunksToHighlight {start:number, end:number}[]
- * @param totalLength number
- * @return {start:number, end:number, highlight:boolean}[]
- */
- const fillInChunks = ({ chunksToHighlight, totalLength }: { chunksToHighlight: Chunk[]; totalLength: number }) => {
- const allChunks: Chunk[] = [];
- const append = (start: number, end: number, highlight: boolean) => {
- if (end - start > 0) {
- allChunks.push({
- start,
- end,
- highlight
- });
- }
- };
- if (chunksToHighlight.length === 0) {
- append(0, totalLength, false);
- } else {
- let lastIndex = 0;
- chunksToHighlight.forEach(chunk => {
- append(lastIndex, chunk.start, false);
- append(chunk.start, chunk.end, true);
- lastIndex = chunk.end;
- });
- append(lastIndex, totalLength, false);
- }
- return allChunks;
- };
- /**
- * Creates an array of chunk objects representing both higlightable and non highlightable pieces of text that match each search word.
- *
- findAll ['z'], 'aaazaaazaaa'
- result #=> [
- { start: 0, end: 3, highlight: false }
- { start: 3, end: 4, highlight: true }
- { start: 4, end: 7, highlight: false }
- { start: 7, end: 8, highlight: true }
- { start: 8, end: 11, highlight: false }
- ]
- findAll ['do', 'dollar'], 'aaa do dollar aaa'
- #=> chunks: [
- { start: 4, end: 6 },
- { start: 7, end: 9 },
- { start: 7, end: 13 },
- ]
- #=> chunksToHight: [
- { start: 4, end: 6 },
- { start: 7, end: 13 },
- ]
- #=> result: [
- { start: 0, end: 4, highlight: false },
- { start: 4, end: 6, highlight: true },
- { start: 6, end: 7, highlight: false },
- { start: 7, end: 13, highlight: true },
- { start: 13, end: 17, highlight: false },
- ]
- * @return Array of "chunks" (where a Chunk is { start:number, end:number, highlight:boolean })
- */
- const findAll = ({
- autoEscape,
- caseSensitive = false,
- searchWords,
- sourceString
- }: ChunkQuery) => {
- if (isString(searchWords)) {
- searchWords = [searchWords];
- }
- const chunks = findChunks({
- autoEscape,
- caseSensitive,
- searchWords,
- sourceString
- });
- const chunksToHighlight = combineChunks({ chunks });
- const result = fillInChunks({
- chunksToHighlight,
- totalLength: sourceString ? sourceString.length : 0
- });
- return result;
- };
- export { findAll };
|