license import type { TextQuoteSelector } from '../types.js' ; import type { Chunk , Chunker , ChunkRange } from './chunker.js' ; import { chunkRangeEquals } from './chunker.js' ; import { textQuoteSelectorMatcher } from './match-text-quote.js' ; import type { RelativeSeeker } from './seeker.js' ; import { TextSeeker } from './seeker.js' ; public export interface DescribeTextQuoteOptions { /**
     * Keep prefix and suffix to the minimum that is necessary to disambiguate
     * the quote. Use only if robustness against text variations is not required.
     */ minimalContext ? : boolean ; /**
     * Add prefix and suffix to quotes below this length, such that the total of
     * `prefix + exact + suffix` is at least this length.
     */ minimumQuoteLength ? : number ; /**
     * When attempting to find a whitespace to make the prefix/suffix start/end
     * (resp.) at a word boundary, give up after this number of characters.
     */ maxWordLength ? : number ; } /**
 * Returns a  TextQuoteSelector that points at the target quote in the
 * given text.
 *
 * The selector will contain the exact target quote. In case this quote appears
 * multiple times in the text, sufficient context around the quote will be
 * included in the selector’s `prefix` and `suffix` attributes to disambiguate.
 * By default, more prefix and suffix are included than strictly required; both
 * in order to be robust against slight modifications, and in an attempt to not
 * end halfway a word (mainly for human readability).
 *
 * This is an abstract implementation of the function’s logic, which expects a
 * generic  Chunker to represent the text, and a  ChunkRange to
 * represent the target.
 *
 * See  dom . describeTextQuote for a wrapper around this
 * implementation which applies it to the text of an HTML DOM.
 *
 *  param target param scope - The text containing the target range; or, more accurately, a
 * function that produces  Chunker s corresponding to this text.
 *  param options returns The  TextQuoteSelector that describes `target`.
 *
 *  public export async function describeTextQuote < TChunk extends Chunk < string > > ( target , scope , options = { } ) { const { minimalContext = false , minimumQuoteLength = 0 , maxWordLength = 50 , } = options ; const seekerAtTarget = new TextSeeker ( scope ( ) ) ; const seekerAtUnintendedMatch = new TextSeeker ( scope ( ) ) ; seekerAtTarget . seekToChunk ( target . startChunk , target . startIndex ) ; const exact = seekerAtTarget . readToChunk ( target . endChunk , target . endIndex ) ; let prefix = '' ; let suffix = '' ; const currentQuoteLength = ( ) => prefix . length + exact . length + suffix . length ; if ( currentQuoteLength ( ) < minimumQuoteLength ) { seekerAtTarget . seekToChunk ( target . startChunk , target . startIndex - prefix . length ) ; const length = Math . floor ( ( minimumQuoteLength - currentQuoteLength ( ) ) / 2 ) ; prefix = seekerAtTarget . read ( - length , false , true ) + prefix ; if ( currentQuoteLength ( ) < minimumQuoteLength ) { seekerAtTarget . seekToChunk ( target . endChunk , target . endIndex + suffix . length ) ; const length = minimumQuoteLength - currentQuoteLength ( ) ; suffix = suffix + seekerAtTarget . read ( length , false , true ) ; if ( currentQuoteLength ( ) < minimumQuoteLength ) { seekerAtTarget . seekToChunk ( target . startChunk , target . startIndex - prefix . length ) ; const length = minimumQuoteLength - currentQuoteLength ( ) ; prefix = seekerAtTarget . read ( - length , false , true ) + prefix ; } } } if ( ! minimalContext ) { seekerAtTarget . seekToChunk ( target . startChunk , target . startIndex - prefix . length ) ; prefix = readUntilWhitespace ( seekerAtTarget , maxWordLength , true ) + prefix ; seekerAtTarget . seekToChunk ( target . endChunk , target . endIndex + suffix . length ) ; suffix = suffix + readUntilWhitespace ( seekerAtTarget , maxWordLength , false ) ; } while ( true ) { const tentativeSelector = { type : 'TextQuoteSelector' , exact , prefix , suffix , } ; const matches = textQuoteSelectorMatcher ( tentativeSelector ) ( scope ( ) ) ; let nextMatch = await matches . next ( ) ; if ( ! nextMatch . done && chunkRangeEquals ( nextMatch . value , target ) ) { nextMatch = await matches . next ( ) ; } if ( nextMatch . done ) return tentativeSelector ; const unintendedMatch = nextMatch . value ; seekerAtTarget . seekToChunk ( target . startChunk , target . startIndex - prefix . length ) ; seekerAtUnintendedMatch . seekToChunk ( unintendedMatch . startChunk , unintendedMatch . startIndex - prefix . length ) ; let extraPrefix = readUntilDifferent ( seekerAtTarget , seekerAtUnintendedMatch , true ) ; if ( extraPrefix !== undefined && ! minimalContext ) extraPrefix = readUntilWhitespace ( seekerAtTarget , maxWordLength , true ) + extraPrefix ; seekerAtTarget . seekToChunk ( target . endChunk , target . endIndex + suffix . length ) ; seekerAtUnintendedMatch . seekToChunk ( unintendedMatch . endChunk , unintendedMatch . endIndex + suffix . length ) ; let extraSuffix = readUntilDifferent ( seekerAtTarget , seekerAtUnintendedMatch , false ) ; if ( extraSuffix !== undefined && ! minimalContext ) extraSuffix = extraSuffix + readUntilWhitespace ( seekerAtTarget , maxWordLength , false ) ; if ( minimalContext ) { if ( extraPrefix !== undefined && ( extraSuffix === undefined || extraPrefix . length <= extraSuffix . length ) ) { prefix = extraPrefix + prefix ; } else if ( extraSuffix !== undefined ) { suffix = suffix + extraSuffix ; } else { throw new Error ( 'Target cannot be disambiguated; how could that have happened‽' ) ; } } else { if ( extraPrefix !== undefined ) prefix = extraPrefix + prefix ; if ( extraSuffix !== undefined ) suffix = suffix + extraSuffix ; } } } function readUntilDifferent ( seeker1 , seeker2 , reverse ) { let result = '' ; while ( true ) { let nextCharacter ; try { nextCharacter = seeker1 . read ( reverse ? - 1 : 1 ) ; } catch ( err ) { return undefined ; } result = reverse ? nextCharacter + result : result + nextCharacter ; let comparisonCharacter ; try { comparisonCharacter = seeker2 . read ( reverse ? - 1 : 1 ) ; } catch ( err ) { if ( ! ( err instanceof RangeError ) ) throw err ; } if ( nextCharacter !== comparisonCharacter ) return result ; } } function readUntilWhitespace ( seeker , limit = Infinity , reverse = false ) { let result = '' ; while ( result . length < limit ) { let nextCharacter ; try { nextCharacter = seeker . read ( reverse ? - 1 : 1 ) ; } catch ( err ) { if ( ! ( err instanceof RangeError ) ) throw err ; break ; } if ( isWhitespace ( nextCharacter ) ) { seeker . seekBy ( reverse ? 1 : - 1 ) ; break ; } result = reverse ? nextCharacter + result : result + nextCharacter ; } return result ; } function isWhitespace ( s ) { return /^\s+$/ . test ( s ) ; }