export type HTMLSelector = readonly [ 'script' , readonly string [ ] ] ; export function extractHTMLSelectorFromRule ( rule ) { if ( rule . startsWith ( '^script' ) === false ) { return undefined ; } const prefix = ':has-text(' ; const selectors = [ ] ; let index = 7 ; while ( rule . startsWith ( prefix , index ) ) { index += prefix . length ; let currentParsingDepth = 1 ; const startOfSelectorIndex = index ; let prev = - 1 ; for ( ; index < rule . length && currentParsingDepth !== 0 ; index += 1 ) { const code = rule . charCodeAt ( index ) ; if ( prev !== 92 ) { if ( code === 40 ) { currentParsingDepth += 1 ; } if ( code === 41 ) { currentParsingDepth -= 1 ; } } prev = code ; } selectors . push ( rule . slice ( startOfSelectorIndex , index - 1 ) ) ; } if ( index !== rule . length ) { return undefined ; } return [ 'script' , selectors ] ; } export function extractTagsFromHtml ( html , tag ) { const tags = [ ] ; const prefix = `<${ tag }` ; const suffix = `</${ tag }>` ; let index = html . indexOf ( prefix ) ; let endOfLastTag = 0 ; while ( index !== - 1 ) { const endOfTagIndex = html . indexOf ( '>' , index + prefix . length ) ; if ( endOfTagIndex === - 1 ) { return [ tags , html . slice ( 0 , index ) , html . slice ( index ) ] ; } if ( html . charCodeAt ( endOfTagIndex - 1 ) === 47 ) { endOfLastTag = endOfTagIndex + 1 ; tags . push ( [ index , html . slice ( index , endOfLastTag ) ] ) ; } else { const indexOfClosingTag = html . indexOf ( suffix , endOfTagIndex ) ; if ( indexOfClosingTag === - 1 ) { return [ tags , html . slice ( 0 , index ) , html . slice ( index ) ] ; } tags . push ( [ index , html . slice ( index , indexOfClosingTag + suffix . length ) ] ) ; endOfLastTag = indexOfClosingTag + suffix . length ; } index = html . indexOf ( prefix , endOfLastTag ) ; } let lastClosingTagIndex = html . lastIndexOf ( '>' ) ; if ( lastClosingTagIndex === - 1 ) { lastClosingTagIndex = endOfLastTag ; } const indexOfNextTag = html . indexOf ( '<' , lastClosingTagIndex ) ; if ( indexOfNextTag === - 1 ) { return [ tags , html , '' ] ; } if ( html . length - indexOfNextTag >= prefix . length || prefix . startsWith ( html . slice ( indexOfNextTag ) ) === false ) { return [ tags , html , '' ] ; } return [ tags , html . slice ( 0 , indexOfNextTag ) , html . slice ( indexOfNextTag ) ] ; } type Patterns = readonly [ readonly string [ ] , readonly RegExp [ ] ] [ ] ; export function extractSelectorsFromRules ( filter ) { const patterns = [ ] ; for ( const [ _ , selectors ] of filter ) { const plainPatterns = [ ] ; const regexpPatterns = [ ] ; for ( const selector of selectors ) { if ( selector . charCodeAt ( 0 ) === 47 ) { if ( selector . endsWith ( '/' ) ) { regexpPatterns . push ( new RegExp ( selector . slice ( 1 , - 1 ) ) ) ; } else if ( selector . endsWith ( '/i' ) ) { regexpPatterns . push ( new RegExp ( selector . slice ( 1 , - 2 ) , 'i' ) ) ; } } else { plainPatterns . push ( selector ) ; } } if ( plainPatterns . length !== 0 || regexpPatterns . length !== 0 ) { patterns . push ( [ plainPatterns , regexpPatterns ] ) ; } } return patterns ; } /**
 * Check if `tag` should be removed from HTML based on `plainPatterns` and
 * `regexpPatterns`. For a tag to be removed, all elements from `plainPatterns`
 * and `regexpPatterns` must match.
 */ function tagShouldBeRemoved ( tag , plainPatterns , regexpPatterns ) { for ( const pattern of plainPatterns ) { if ( tag . indexOf ( pattern ) === - 1 ) { return false ; } } for ( const pattern of regexpPatterns ) { if ( pattern . test ( tag ) === false ) { return false ; } } return true ; } export function selectTagsToRemove ( patterns , tags ) { const toRemove = [ ] ; for ( const tag of tags ) { for ( const [ plainPatterns , regexpPatterns ] of patterns ) { if ( tagShouldBeRemoved ( tag [ 1 ] , plainPatterns , regexpPatterns ) ) { toRemove . push ( tag ) ; break ; } } } return toRemove ; } export function removeTagsFromHtml ( html , toRemove ) { if ( toRemove . length === 0 ) { return html ; } let filteredHtml = html ; toRemove . reverse ( ) ; for ( const [ index , tag ] of toRemove ) { filteredHtml = filteredHtml . slice ( 0 , index ) + filteredHtml . slice ( index + tag . length ) ; } return filteredHtml ; } export default class StreamingHtmlFilter { private buffer : string ; private readonly patterns : Patterns ; constructor ( selectors ) { this . buffer = '' ; this . patterns = extractSelectorsFromRules ( selectors ) ; } public flush ( ) { return this . buffer ; } public write ( chunk ) { if ( this . patterns . length === 0 ) { return chunk ; } if ( chunk . length === 0 ) { return chunk ; } this . buffer += chunk ; const [ tags , parsed , rest ] = extractTagsFromHtml ( this . buffer , 'script' ) ; this . buffer = rest ; if ( tags . length === 0 ) { return parsed ; } return removeTagsFromHtml ( parsed , selectTagsToRemove ( this . patterns , tags ) ) ; } }