import { ExportNs } from '../../esl-utils/environment/export-ns' ; import { tuple , wrap , uniq } from '../../esl-utils/misc/array' ; import { unwrapParenthesis } from '../../esl-utils/misc/format' ; import { findAll , findChildren , findNext , findParent , findPrev } from '../../esl-utils/dom/traversing' ; type ProcessorDescriptor = [ string ? , string ? ] ; type ElementProcessor = ( base : Element , sel : string ) => Element | Element [ ] | null ; type CollectionProcessor = ( els : Element [ ] , sel : string ) => Element [ ] ; example @ ExportNs ( 'TraversingQuery' ) export class TraversingQuery { private static ELEMENT_PROCESSORS : Record < string , ElementProcessor > = { '::find' : findAll , '::next' : findNext , '::prev' : findPrev , '::child' : findChildren , '::parent' : findParent } ; private static COLLECTION_PROCESSORS : Record < string , CollectionProcessor > = { '::first' : ( list ) => list . slice ( 0 , 1 ) , '::last' : ( list ) => list . slice ( - 1 ) , '::nth' : ( list , sel ? ) => { const index = sel ? + sel : NaN ; return wrap ( list [ index - 1 ] ) ; } , '::not' : ( list , sel ? ) => list . filter ( ( el ) => ! el . matches ( sel || '' ) ) , '::filter' : ( list , sel ? ) => list . filter ( ( el ) => el . matches ( sel || '' ) ) } ; returns private static get PROCESSORS_REGEX ( ) : RegExp { const keys = Object . keys ( this . ELEMENT_PROCESSORS ) . concat ( Object . keys ( this . COLLECTION_PROCESSORS ) ) ; return new RegExp ( `(${ keys . join ( '|' ) })` , 'g' ) ; } private static isCollectionProcessor ( [ name ] ) { return ! ! name && ( name in this . COLLECTION_PROCESSORS ) ; } private static processElement ( el , [ name , selString ] ) { const sel = unwrapParenthesis ( selString || '' ) ; if ( ! name || ! ( name in this . ELEMENT_PROCESSORS ) ) return [ ] ; return wrap ( this . ELEMENT_PROCESSORS [ name ] ( el , sel ) ) ; } private static processCollection ( els , [ name , selString ] ) { const sel = unwrapParenthesis ( selString || '' ) ; if ( ! name || ! ( name in this . COLLECTION_PROCESSORS ) ) return [ ] ; return wrap ( this . COLLECTION_PROCESSORS [ name ] ( els , sel ) ) ; } private static traverseChain ( collection , processors , findFirst ) { if ( ! processors . length || ! collection . length ) return collection ; const [ processor , ... rest ] = processors ; if ( this . isCollectionProcessor ( processor ) ) { const processedItem = this . processCollection ( collection , processor ) ; return this . traverseChain ( processedItem , rest , findFirst ) ; } const result = [ ] ; for ( const target of collection ) { const processedItem = this . processElement ( target , processor ) ; const resultCollection = this . traverseChain ( processedItem , rest , findFirst ) ; if ( ! resultCollection . length ) continue ; if ( findFirst ) return resultCollection . slice ( 0 , 1 ) ; result . push ( ... resultCollection ) ; } return uniq ( result ) ; } static traverse ( query , findFirst , base ? , scope = document ) { const parts = query . split ( this . PROCESSORS_REGEX ) . map ( ( term ) => term . trim ( ) ) ; const rootSel = parts . shift ( ) ; const baseCollection = base ? [ base ] : [ ] ; const initial = rootSel ? Array . from ( scope . querySelectorAll ( rootSel ) ) : baseCollection ; return this . traverseChain ( initial , tuple ( parts ) , findFirst ) ; } returns first matching element reached via  TraversingQuery rules  static first ( query , base ? , scope ? ) { return TraversingQuery . traverse ( query , true , base , scope ) [ 0 ] || null ; } returns Array of all matching elements reached via  TraversingQuery rules  static all ( query , base ? , scope ? ) { return TraversingQuery . traverse ( query , false , base , scope ) ; } } declare global { export interface ESLLibrary { TraversingQuery : typeof TraversingQuery ; } }