import { Value } from "classnames" ; import { ID , RealID , Field , Reference , trap , assert , Token , cast , arrayLast , Text , assertDefined , _Number , _Array , Entry , Record , FieldID , Version , Nil , Base , Character , Head , isNumber , CompileError , Item , Path , FormulaType } from "./exports" ; /** evaluate edit operations */ export function edit ( version ) { const targetRef = cast ( version . get ( '^target' ) . value , Reference ) ; assert ( targetRef . dependent ) ; const input = version . workspace . down ( targetRef . path . ids . slice ( 0 , targetRef . context ) ) ; version . copyValue ( input ) ; const head = cast ( version . value , Head ) ; const versionPathLength = version . path . length ; for ( let item of head . visit ( ) ) { if ( item . originalEditError !== 'conversion' ) { item . editError = undefined ; } } const targetPath = targetRef . path . ids . slice ( targetRef . context ) ; const target = version . down ( targetPath ) ; assert ( target ) ; assert ( target . io === 'data' ) ; /** editor function to iterate */ let editor ; /** post-edit cleanup function */ /** Iterate edit over nested arrays */ function iterateEdit ( context , path , editor ) { let index = path . findIndex ( isNumber ) ; if ( index >= 0 ) { assert ( path [ index ] === 0 ) ; let array = context . down ( path . slice ( 0 , index ) ) . value ; assert ( array instanceof _Array ) ; let arrayPath = path . slice ( index + 1 ) ; iterateEdit ( array . template , arrayPath , editor ) ; array . items . forEach ( item => iterateEdit ( item , arrayPath , editor ) ) ; return ; } const target = context . down ( path ) ; editor ( target ) ; } /** copy from source to target. If target is inside an array entry, will copy
     * from its template instead, which should already have been copied from
     * source. Includes source metadata. Does not change target id or io. Returns
     * actual source of copy */ function templateCopy ( target , source ) { let templatePath = source . path ; let entryPath = target . path ; let ids = target . path . ids . slice ( versionPathLength ) . reverse ( ) ; let arrayIndex = ids . findIndex ( id => isNumber ( id ) && id > 0 ) ; if ( arrayIndex >= 0 ) { let templateIds = target . path . ids . slice ( ) ; assert ( templateIds [ templateIds . length - 1 - arrayIndex ] > 0 ) ; templateIds [ templateIds . length - 1 - arrayIndex ] = 0 ; source = target . workspace . down ( templateIds ) ; templatePath = source . path . up ( arrayIndex ) ; assert ( arrayLast ( templatePath . ids ) === 0 ) ; entryPath = target . path . up ( arrayIndex ) ; assert ( arrayLast ( entryPath . ids ) > 0 ) ; } let temp = source . copy ( templatePath , entryPath ) ; target . metadata = temp . metadata ; if ( target . metadata ) target . metadata . containingItem = target ; target . formulaType = temp . formulaType ; target . detachValueIf ( ) ; target . value = temp . value ; if ( target . value ) target . value . containingItem = target ; return source ; } /** Disallow references outside literal value */ function literalCheck ( literal ) { for ( let item of literal . visit ( ) ) { if ( item . value instanceof Reference && ! literal . contains ( item . value . target ! ) ) { throw new CompileError ( item , 'reference escaping literal value' ) ; } } } switch ( version . formulaType ) { case '::nochange' : { editor = ( ) => { } ; break ; } case '::replace' : { let source = version . get ( '^source' ) ; if ( source . value instanceof Reference ) { source = assertDefined ( source . value . target ) ; } else { literalCheck ( source ) ; } editor = ( target ) => { templateCopy ( target , source ) ; } ; break ; } case '::append' : { if ( ! ( target . value instanceof Record ) ) { throw new CompileError ( target , 'can only append to record' ) ; } const sourceField = cast ( version . get ( '^source' ) . value , Record ) . fields [ 0 ] ; assert ( sourceField ) ; let valueItem = sourceField ; if ( valueItem . value instanceof Reference ) { valueItem = assertDefined ( valueItem . value . target ) ; } else { literalCheck ( valueItem ) ; } editor = ( target ) => { let newField = new Field ; cast ( target . value , Record ) . add ( newField ) ; newField . id = sourceField . id ; newField . io = sourceField . io ; templateCopy ( newField , valueItem ) ; } ; break ; } case '::insert' : { if ( ! ( target . container instanceof Record ) ) { throw new CompileError ( target , 'can only insert into record' ) ; } const sourceField = cast ( version . get ( '^source' ) . value , Record ) . fields [ 0 ] ; assert ( sourceField ) ; let valueItem = sourceField ; if ( valueItem . value instanceof Reference ) { valueItem = assertDefined ( valueItem . value . target ) ; } else { literalCheck ( valueItem ) ; } editor = ( target ) => { let newField = new Field ; newField . id = sourceField . id ; newField . io = sourceField . io ; let record = target . container as Record ; newField . container = record ; let i = record . fields . indexOf ( target as Field ) ; assert ( i >= 0 ) ; record . fields . splice ( i , 0 , newField ) ; templateCopy ( newField , valueItem ) ; } ; break ; } case '::delete' : { if ( ! ( target instanceof Field ) ) { throw new CompileError ( target , 'can only delete a field' ) ; } editor = ( item ) => ( item as Field ) . delete ( ) ; break ; } case '::move' : case '::move-append' : case '::move-insert' : { const sourceRef = cast ( version . get ( '^source' ) . value , Reference ) ; if ( ! sourceRef . target ) { throw new CompileError ( undefined , 'invalid reference' ) ; } if ( ! ( sourceRef . target instanceof Field ) ) { throw new CompileError ( undefined , 'can only move a field' ) ; } assert ( sourceRef . dependent ) ; const x = version . down ( sourceRef . path . ids . slice ( sourceRef . context ) ) ; assert ( x instanceof Field ) ; const source = x ; const lubLength = target . path . lub ( source . path ) . length ; if ( target . path . ids . slice ( lubLength ) . find ( isNumber ) || source . path . ids . slice ( lubLength ) . find ( isNumber ) ) { trap ( ) ; } const sourceSuffix = source . path . ids . slice ( lubLength ) ; let movedPath = target . path ; /** perform move edit iterated over array entries by iteratedEdit */ function templateMove ( to , from ) { let templateSource = templateCopy ( to , from ) ; if ( templateSource === source ) { source . delete ( ) ; let moved = new Reference ; moved . path = movedPath ; moved . context = versionPathLength ; moved . guards = target . path . ids . map ( ( ) => undefined ) ; moved . tokens = target . path . ids . slice ( moved . context ) . map ( id => { let name = id . toString ( ) ; return new Token ( 'name' , 0 , name . length , name ) ; } ) ; source . setMeta ( '^moved' , moved ) ; } else { let entrySource = source . workspace . down ( [ ... to . path . ids . slice ( 0 , lubLength ) , ... sourceSuffix ] ) ; assert ( entrySource instanceof Field ) ; to . detachValueIf ( ) ; if ( entrySource . value ) { to . copyValue ( entrySource ) ; } entrySource . delete ( ) ; templateCopy ( entrySource , source ) ; let moved = assertDefined ( entrySource . get ( '^moved' ) ) ; moved . eval ( ) ; assert ( cast ( moved . value , Reference ) . target === to ) ; } } if ( version . formulaType === '::move' ) { editor = ( target ) => { templateMove ( target , source ) ; target . io = source . io ; } ; } else { const newID = new FieldID ( version . id . serial + 1 ) ; newID . name = source . id . name ; if ( version . formulaType === '::move-append' ) { if ( ! ( target . value instanceof Record ) ) { throw new CompileError ( target , 'can only append to record' ) ; } movedPath = movedPath . down ( newID ) ; editor = ( target ) => { let newField = new Field ; newField . id = newID ; newField . io = source . io ; cast ( target . value , Record ) . add ( newField ) ; templateMove ( newField , source ) ; } ; } else { assert ( version . formulaType === '::move-insert' ) ; if ( ! ( target . container instanceof Record ) ) { throw new CompileError ( target , 'can only insert into record' ) ; } movedPath = movedPath . up ( 1 ) . down ( newID ) ; editor = ( target ) => { let newField = new Field ; newField . id = newID ; newField . io = source . io ; let record = target . container as Record ; newField . container = record ; let i = record . fields . indexOf ( target as Field ) ; assert ( i >= 0 ) ; record . fields . splice ( i , 0 , newField ) ; templateMove ( newField , source ) ; } ; } } break ; } case '::wrap-record' : case '::wrap-array' : { const record = version . formulaType === '::wrap-record' ; const source = target ; let newID ; if ( record ) { newID = new FieldID ( version . id . serial + 1 ) ; newID . name = 'value' ; } else { newID = 0 ; } editor = ( target ) => { const movedPath = target . path . down ( newID ) ; let copy = target . copy ( target . path , movedPath ) ; let item ; if ( record ) { item = new Field ; item . io = copy . io ; item . formulaType = copy . formulaType ; } else { item = new Entry ; assert ( target . dataLike && target . formulaType === 'none' ) ; item . io = 'data' ; item . formulaType = 'none' ; } item . id = newID ; item . conditional = copy . conditional ; item . editError = copy . editError ; item . metadata = copy . metadata ; if ( item . metadata ) item . metadata . containingItem = item ; item . value = copy . value ; if ( item . value ) item . value . containingItem = item ; target . detachValueIf ( ) ; if ( record ) { let rec = new Record ; rec . add ( item as Field ) ; target . setValue ( rec ) ; } else { let array = new _Array ; array . template = item as Entry ; item . container = array ; target . setValue ( array ) ; let entry = array . createEntry ( ) ; entry . copyValue ( item ) ; if ( target !== source ) { item . detachValue ( ) ; item . copyValue ( ( source . value as _Array ) . template ) ; } } target . metadata = undefined ; target . io = 'data' ; target . formulaType = 'none' ; target . conditional = false ; target . usesPrevious = false ; target . editError = undefined ; } ; break ; } case '::unwrap' : { editor = ( target ) => { let items ; let value = target . value ; if ( value instanceof Record ) { items = value . fields . filter ( field => ! field . deleted ) ; } else if ( value instanceof _Array ) { items = value . items ; } else { trap ( ) ; } if ( items . length !== 1 ) { target . editError = 'conversion' ; } let item = items [ 0 ] ; if ( ! item ) { if ( value instanceof _Array ) { item = value . template ; } else { target . detachValueIf ( ) ; target . setValue ( new Nil ) ; target . metadata = undefined ; target . formulaType = 'none' ; return ; } } let copy = item . copy ( item . path , target . path ) ; target . detachValueIf ( ) ; if ( copy . value ) { target . value = copy . value ; target . value . containingItem = target ; } target . metadata = copy . metadata ; if ( target . metadata ) { target . metadata . containingItem = target ; } target . io = copy . io ; target . formulaType = copy . formulaType ; return ; } ; break ; } case '::convert' : { const to = version . get ( '^source' ) ; literalCheck ( to ) ; const toType = assertDefined ( to . value ) ; if ( ! ( toType instanceof Base || toType instanceof Text ) ) { throw new CompileError ( to , 'can only convert to base types and text' ) ; } editor = ( target ) => { let fromVal = assertDefined ( target . value ) ; target . detachValue ( ) ; target . editError = undefined ; if ( toType instanceof Reference ) trap ( ) ; if ( toType instanceof Nil ) { target . setValue ( new Nil ) ; return ; } if ( fromVal instanceof Nil ) { target . copyValue ( to ) ; return ; } if ( toType instanceof Text ) { if ( fromVal instanceof Text ) { target . setValue ( fromVal ) ; return ; } let text = new Text ; target . setValue ( text ) ; text . value = toType . value ; if ( fromVal instanceof _Number ) { if ( fromVal . isBlank ( ) ) { } else { text . value = fromVal . value . toString ( ) ; } return ; } if ( fromVal instanceof Character ) { text . value = fromVal . value ; return ; } return ; } if ( toType instanceof _Number ) { if ( fromVal instanceof _Number ) { target . setValue ( fromVal ) ; return ; } let num = new _Number ; target . setValue ( num ) ; if ( fromVal instanceof Character ) { num . value = fromVal . value . charCodeAt ( 0 ) ; return ; } if ( fromVal instanceof Text ) { let text = fromVal . value . trim ( ) ; if ( text ) { num . value = Number ( text ) ; if ( Number . isNaN ( num . value ) ) { target . editError = 'conversion' ; } } else { num . value = Number . NaN ; } return ; } num . value = toType . value ; return ; } trap ( ) ; } ; break ; } default : trap ( ) ; } iterateEdit ( version , targetPath , editor ) ; version . workspace . analyze ( version ) ; } /** Abstract edit description. Pulled out of a Version, with the target relative
 * reference converted to a Path */ type Edit = { /** path at which to apply edit */ target : Path ; /** type of edit */ type : FormulaType ; /** source of some edits, either a literal base value or a path */ source ? : Base | Path ; } ; /** pull an edit through another one, where they both apply to the same base
 * state. Produces 2 new edits such that this diagram commutes:
 *
 *      branch
 * o------------->o
 * |              |
 * |delta         |newDelta
 * |              |
 * v              v
 * o------------->o
 *    newBranch
 *
 * */ export function pullEdit ( delta , branch ) { if ( delta . type === '::nochange' || branch . type === '::nochange' ) { return { newDelta : delta , newBranch : branch } ; } if ( delta . target . equals ( branch . target ) && delta . type === branch . type && ( ( delta . source === undefined && branch . source === undefined ) || ( delta . source instanceof Path && branch . source instanceof Path && delta . source . equals ( branch . source ) ) || ( delta . source instanceof Base && delta . source . equals ( branch . source ) ) ) ) { let nochange = { target : delta . target , type : '::nochange' } ; return { newDelta : nochange , newBranch : nochange } ; } trap ( ) ; }