license import type { TextQuoteSelector } from '../types.js' ; import type { Chunk , Chunker , ChunkRange } from './chunker.js' ; /**
 * Find occurrences in a text matching the given  TextQuoteSelector .
 *
 * This performs an exact search the selector’s quote (including prefix and
 * suffix) within the given text.
 *
 * Note the match is based on strict character-by-character equivalence, i.e.
 * it is sensitive to whitespace, capitalisation, etc.
 *
 * This is an abstract implementation of the function’s logic, which expects a
 * generic  Chunker to represent the text, and returns an (async)
 * generator of  ChunkRange s to represent the matches.
 *
 * See  dom . createTextQuoteSelectorMatcher for a
 * wrapper around this implementation which applies it to the text of an HTML
 * DOM.
 *
 * The function is curried, taking first the selector and then the text.
 *
 * As there may be multiple matches for a given selector (when its prefix and
 * suffix attributes are not sufficient to disambiguate it), the matcher will
 * return an (async) generator that produces each match in the order they are
 * found in the text.
 *
 * *XXX Modifying the Chunks while the search is still running can mess up and
 * result in an error or an infinite loop. See [issue #112](https://github.com/apache/incubator-annotator/issues/112).*
 *
 *  example param selector - The  TextQuoteSelector to be anchored
 *  returns a  Matcher function that applies `selector` to a given text
 *
 *  public export function textQuoteSelectorMatcher ( selector ) { return async function * matchAll < TChunk extends Chunk < string > > ( textChunks ) { const exact = selector . exact ; const prefix = selector . prefix || '' ; const suffix = selector . suffix || '' ; const searchPattern = prefix + exact + suffix ; interface PartialMatch { startChunk ? : TChunk ; startIndex ? : number ; endChunk ? : TChunk ; endIndex ? : number ; charactersMatched : number ; } let partialMatches = [ ] ; let isFirstChunk = true ; do { const chunk = textChunks . currentChunk ; const chunkValue = chunk . data ; const remainingPartialMatches = [ ] ; for ( const partialMatch of partialMatches ) { const charactersMatched = partialMatch . charactersMatched ; if ( partialMatch . endChunk === undefined ) { const charactersUntilMatchEnd = prefix . length + exact . length - charactersMatched ; if ( charactersUntilMatchEnd <= chunkValue . length ) { partialMatch . endChunk = chunk ; partialMatch . endIndex = charactersUntilMatchEnd ; } } if ( partialMatch . startChunk === undefined ) { const charactersUntilMatchStart = prefix . length - charactersMatched ; if ( charactersUntilMatchStart < chunkValue . length || partialMatch . endChunk !== undefined ) { partialMatch . startChunk = chunk ; partialMatch . startIndex = charactersUntilMatchStart ; } } const charactersUntilSuffixEnd = searchPattern . length - charactersMatched ; if ( charactersUntilSuffixEnd <= chunkValue . length ) { if ( chunkValue . startsWith ( searchPattern . substring ( charactersMatched ) ) ) { yield partialMatch as ChunkRange < TChunk > ; } } else if ( chunkValue === searchPattern . substring ( charactersMatched , charactersMatched + chunkValue . length ) ) { partialMatch . charactersMatched += chunkValue . length ; remainingPartialMatches . push ( partialMatch ) ; } } partialMatches = remainingPartialMatches ; if ( searchPattern . length <= chunkValue . length ) { let fromIndex = 0 ; while ( fromIndex <= chunkValue . length ) { const patternStartIndex = chunkValue . indexOf ( searchPattern , fromIndex ) ; if ( patternStartIndex === - 1 ) break ; fromIndex = patternStartIndex + 1 ; if ( patternStartIndex === 0 && searchPattern . length === 0 && ! isFirstChunk ) continue ; yield { startChunk : chunk , startIndex : patternStartIndex + prefix . length , endChunk : chunk , endIndex : patternStartIndex + prefix . length + exact . length , } ; } } let newPartialMatches = [ ] ; const searchStartPoint = Math . max ( chunkValue . length - searchPattern . length + 1 , 0 ) ; for ( let i = searchStartPoint ; i < chunkValue . length ; i ++ ) { const character = chunkValue [ i ] ; newPartialMatches = newPartialMatches . filter ( ( partialMatchStartIndex ) => character === searchPattern [ i - partialMatchStartIndex ] ) ; if ( character === searchPattern [ 0 ] ) newPartialMatches . push ( i ) ; } for ( const partialMatchStartIndex of newPartialMatches ) { const charactersMatched = chunkValue . length - partialMatchStartIndex ; const partialMatch = { charactersMatched , } ; if ( charactersMatched >= prefix . length + exact . length ) { partialMatch . endChunk = chunk ; partialMatch . endIndex = partialMatchStartIndex + prefix . length + exact . length ; } if ( charactersMatched > prefix . length || partialMatch . endChunk !== undefined ) { partialMatch . startChunk = chunk ; partialMatch . startIndex = partialMatchStartIndex + prefix . length ; } partialMatches . push ( partialMatch ) ; } isFirstChunk = false ; } while ( textChunks . nextChunk ( ) !== null ) ; } ; }