import { Context } from './Context' ; import { Node } from './Node' ; const FastPriorityQueue = require ( 'fastpriorityqueue' ) ; export type CrawlEvent = 'INVALID' | 'END' | 'IMPLICIT_END' | 'MAX_DEPTH_EXCEEDED' | 'VISIT_LIMIT_EXCEEDED' | 'ALREADY_SEEN' ; export interface CrawlEntry < C extends Context > { depth : number ; node : Node < C > | null ; prevId : string ; prevLine : number ; prevNodeStr : string ; fromAction : string | number ; } interface CrawlPriorityQueue < C extends Context > { add : ( v : CrawlEntry < C > ) => void ; peek : ( ) => CrawlEntry < C > ; poll : ( ) => CrawlEntry < C > ; size : number ; } function getNodeLine ( node ) { if ( node === null ) { return - 1 ; } try { return parseInt ( node . elem . attr ( 'data-line' ) , 10 ) ; } catch ( e ) { return - 1 ; } } export abstract class CrawlerBase < C extends Context > { protected seen : Set < string > ; protected lineVisitCount : { [ line ] : number ; } ; protected queue : CrawlPriorityQueue < C > ; constructor ( ) { this . seen = new Set ( ) ; this . lineVisitCount = { } ; this . queue = new FastPriorityQueue ( ( a , b ) => { return ( this . lineVisitCount [ getNodeLine ( a . node ) ] || 0 ) < ( this . lineVisitCount [ getNodeLine ( b . node ) ] || 0 ) ; } ) ; } public crawl ( root ? , timeLimitMillis = 500 , depthLimit = 150 , visitLimit = 10 ) { return this . traverse ( root , timeLimitMillis , depthLimit , visitLimit ) ; } protected calculateAddedDepth ( n ) { return 1 ; } protected abstract onEvent ( q , e ) ; protected abstract onNode ( q , nodeStr , id , line ) ; protected abstract onErrors ( q , errors , line ) ; private traverse ( root ? , timeLimitMillis ? , depthLimit ? , visitLimit ? ) { if ( root ) { this . queue . add ( { depth : 0 , node : root , prevId : 'START' , prevLine : - 1 , prevNodeStr : 'START' , fromAction : 'START' , } ) ; } const start = Date . now ( ) ; while ( this . queue . size > 0 && ( ! timeLimitMillis || ( Date . now ( ) - start ) < timeLimitMillis ) ) { const q = this . queue . poll ( ) ; if ( depthLimit && q . depth >= depthLimit ) { this . onEvent ( q , 'MAX_DEPTH_EXCEEDED' ) ; continue ; } if ( q . node === undefined || q . node === null ) { this . onEvent ( q , 'IMPLICIT_END' ) ; continue ; } const id = q . node . elem . attr ( 'id' ) || q . prevId ; const line = parseInt ( q . node . elem . attr ( 'data-line' ) , 10 ) ; this . lineVisitCount [ line ] = ( this . lineVisitCount [ line ] || 0 ) + 1 ; if ( visitLimit && this . lineVisitCount [ line ] >= visitLimit ) { this . onEvent ( q , 'VISIT_LIMIT_EXCEEDED' ) ; continue ; } if ( isNaN ( line ) ) { this . onEvent ( q , 'INVALID' ) ; continue ; } if ( q . node . getTag ( ) === 'trigger' && q . node . elem . text ( ) . trim ( ) === 'end' ) { this . onEvent ( q , 'END' ) ; continue ; } const nstr = q . node . getComparisonKey ( ) ; if ( this . seen . has ( nstr ) ) { this . onEvent ( q , 'ALREADY_SEEN' ) ; continue ; } this . seen . add ( nstr ) ; this . onNode ( q , nstr , id , line ) ; const keys = q . node . getVisibleKeys ( ) ; if ( keys . length === 0 ) { keys . push ( 0 ) ; } for ( const k of keys ) { this . queue . add ( { depth : q . depth + this . calculateAddedDepth ( q . node ) , node : q . node . handleAction ( k ) , prevId : id , prevLine : line , prevNodeStr : nstr , fromAction : k , } ) ; } const errors = q . node . getErrors ( ) ; if ( errors . length > 0 ) { this . onErrors ( q , errors , line ) ; } } return ( this . queue . size > 0 ) ; } }