import { Annotation , AnnotationJSON , Document , Join } from "./internals" ; export function compareAnnotations ( a , b ) { let startDelta = a . start - b . start ; let endDelta = b . end - a . end ; let rankDelta = a . rank - b . rank ; if ( startDelta === 0 ) { if ( endDelta === 0 ) { if ( rankDelta === 0 ) { return a . type > b . type ? 1 : a . type < b . type ? - 1 : 0 ; } else { return rankDelta ; } } else { return endDelta ; } } else { return startDelta ; } } function matches ( annotation , filter ) { return Object . keys ( filter ) . every ( function matchesTest ( key ) { let value = filter [ key ] ; if ( typeof value === "object" ) { return matches ( ( annotation as any ) [ key ] , value ) ; } return ( annotation as any ) [ key ] === value ; } ) ; } export class Collection { document : Document ; annotations : Array < Annotation < any > > ; constructor ( document , annotations ) { this . document = document ; this . annotations = annotations ; } * [ Symbol . iterator ] ( ) { for ( let annotation of this . annotations ) { yield annotation ; } } get length ( ) { return this . annotations . length ; } map < T > ( mapper ) { return this . annotations . map ( mapper ) ; } forEach ( callback ) { this . annotations . forEach ( callback ) ; } reduce < T > ( reducer , initialValue ) { return this . annotations . reduce ( reducer , initialValue ) ; } sort ( sortFunction = compareAnnotations ) { this . annotations = [ ... this . annotations ] . sort ( sortFunction ) ; return this ; } where ( filter ) { if ( filter instanceof Function ) { return new AnnotationCollection ( this . document , this . annotations . filter ( filter ) ) ; } /**
         * this is just inlining the logic for the common case where the filter object looks like `{ type: '-vendor-type' }` for performance reasons
         */ if ( Object . keys ( filter ) . length === 1 && filter . type != null ) { let annotations = [ ] ; for ( let a of this . annotations ) { if ( filter . type === `-${ a . vendorPrefix }-${ a . type }` || ( a . type === "unknown" && a . vendorPrefix === "atjson" && a . attributes . type === filter . type ) ) { annotations . push ( a ) ; } } return new AnnotationCollection ( this . document , annotations ) ; } let annotations = this . annotations . filter ( function jsonMatchesFilter ( annotation ) { return matches ( annotation . toJSON ( ) , filter ) ; } ) ; return new AnnotationCollection ( this . document , annotations ) ; } as < T extends string > ( name ) { return new NamedCollection < T > ( this . document , this . annotations , name ) ; } update ( updater ) { let newAnnotations = [ ] ; for ( let annotation of this . annotations ) { let updateObject = updater ( annotation ) ; if ( updateObject ) { if ( updateObject . add ) newAnnotations . push ( ... updateObject . add ) ; if ( updateObject . update ) newAnnotations . push ( ... updateObject . update . map ( function get1 ( a ) { return a [ 1 ] ; } ) ) ; if ( updateObject . retain ) newAnnotations . push ( ... updateObject . retain ) ; } } this . annotations = newAnnotations ; return this ; } toJSON ( ) { return this . map ( function annotationToJSON ( a ) { return a . toJSON ( ) ; } ) ; } } export interface Renaming { [ key ] : string | Renaming ; } export interface FlattenedRenaming { [ key ] : string ; } function flattenPropertyPaths ( mapping , options , prefix ? ) { let result = { } ; for ( let key in mapping ) { let value = mapping [ key ] ; let fullyQualifiedKey = key ; if ( prefix ) { fullyQualifiedKey = `${ prefix }.${ key }` ; if ( options . values ) { value = `${ prefix }.${ value }` ; } } if ( typeof value !== "object" ) { result [ fullyQualifiedKey ] = value ; } else { Object . assign ( result , flattenPropertyPaths ( value , options , fullyQualifiedKey ) ) ; } } return result ; } function without ( object , attributes ) { let copy = { } ; for ( let key in object ) { let activeAttributes = attributes . filter ( function startsWithKey ( attribute ) { return attribute . split ( "." ) [ 0 ] === key ; } ) ; if ( activeAttributes . length === 0 ) { copy [ key ] = object [ key ] ; } else if ( activeAttributes . indexOf ( key ) === - 1 ) { copy [ key ] = without ( object [ key ] , activeAttributes . map ( function removeFirstKey ( attribute ) { return attribute . split ( "." ) . slice ( 1 ) . join ( "." ) ; } ) ) ; } } return copy ; } function get ( object , key ) { if ( key === "" ) return object ; let [ path , ... rest ] = key . split ( "." ) ; if ( object ) { return get ( object [ path ] , rest . join ( "." ) ) ; } return null ; } function set ( object , key , value ) { if ( value == null ) return ; let [ path , ... rest ] = key . split ( "." ) ; if ( rest . length === 0 ) { object [ path ] = value ; } else { if ( object [ path ] == null ) { object [ path ] = { } ; } set ( object [ path ] , rest . join ( "." ) , value ) ; } } export class AnnotationCollection extends Collection { set ( patch ) { let flattenedPatch = flattenPropertyPaths ( patch , { keys : true } ) ; let patchAnnotationUpdater = ( annotation ) => { let result = annotation . toJSON ( ) as AnnotationJSON ; for ( let key in flattenedPatch ) { set ( result , key , flattenedPatch [ key ] ) ; } let newAnnotation = this . document . replaceAnnotation ( annotation , result ) ; let r = { update : [ [ annotation , newAnnotation [ 0 ] ] ] , } ; return r ; } ; return this . update ( patchAnnotationUpdater ) ; } unset ( ... keys ) { let unsetKeysUpdater = ( annotation ) => { let result = without ( annotation . toJSON ( ) , keys ) as AnnotationJSON ; let newAnnotation = this . document . replaceAnnotation ( annotation , result ) ; let r = { update : [ [ annotation , newAnnotation [ 0 ] ] ] , } ; return r ; } ; return this . update ( unsetKeysUpdater ) ; } rename ( renaming ) { let flattenedRenaming = flattenPropertyPaths ( renaming , { keys : true , values : true , } ) ; let renameUpdater = ( annotation ) => { let json = annotation . toJSON ( ) as AnnotationJSON ; let result = without ( annotation . toJSON ( ) , Object . keys ( flattenedRenaming ) ) ; for ( let key in flattenedRenaming ) { let value = get ( json , key ) ; set ( result , flattenedRenaming [ key ] , value ) ; } let newAnnotation = this . document . replaceAnnotation ( annotation , result ) ; let r = { update : [ [ annotation , newAnnotation [ 0 ] ] ] , } ; return r ; } ; return this . update ( renameUpdater ) ; } remove ( ) { this . document . removeAnnotations ( this . annotations ) ; this . annotations = [ ] ; } } export class NamedCollection < Left extends string > extends Collection { readonly name : Left ; constructor ( document , annotations , name ) { super ( document , annotations ) ; this . name = name ; } outerJoin < Right extends string > ( rightCollection , filter ) { if ( rightCollection . document !== this . document ) { throw new Error ( "Joining annotations from two different documents is non-sensical. Refusing to continue." ) ; } let results = new Join < Left , Right > ( this , [ ] ) ; for ( let leftAnnotation of this . annotations ) { let joinAnnotations = rightCollection . annotations . filter ( function testJoinCandidates ( rightAnnotation ) { return filter ( leftAnnotation , rightAnnotation ) ; } ) ; type JoinItem = Record < Left , Annotation < any > > & Record < Right , Array < Annotation < any > > > ; let join = { [ this . name ] : leftAnnotation , [ rightCollection . name ] : joinAnnotations , } ; results . push ( join as JoinItem ) ; } return results ; } join < Right extends string > ( rightCollection , filter ) { return this . outerJoin ( rightCollection , filter ) . where ( function testRightCollectionLength ( record ) { return record [ rightCollection . name ] . length > 0 ; } ) ; } }