import { assert } from "@fluidframework/common-utils" ; import { createInsertSegmentOp , Marker , MergeTreeDeltaType , ReferenceType , reservedRangeLabelsKey , TextSegment , ISegment , IMergeTreeDeltaOp , } from "@fluidframework/merge-tree" ; import { SharedString , SequenceDeltaEvent , ISequenceDeltaRange , } from "@fluidframework/sequence" ; import { Schema , Fragment , Slice , } from "prosemirror-model" ; import { EditorState , Transaction } from "prosemirror-state" ; import { ReplaceAroundStep } from "prosemirror-transform" ; export interface IProseMirrorNode { [ key ] : any ; type : string ; content ? : IProseMirrorNode [ ] ; marks ? : any [ ] ; _open ? : boolean ; } export interface IProseMirrorSlice { openStart ? : number ; openEnd ? : number ; content : IProseMirrorNode [ ] ; } export const proseMirrorTreeLabel = "prosemirror" ; export const nodeTypeKey = "nodeType" ; export class IProseMirrorTransaction { } interface IThing { type : "ether" | "delete" | "insert" | "annotate" ; event ? : ISequenceDeltaRange ; length : number ; annotations ? : any ; } interface IThingGroup { items : IThing [ ] ; position : number ; } export class ProseMirrorTransactionBuilder { private readonly transaction : Transaction ; private readonly things = new Array < IThing > ( ) ; constructor ( state , private readonly schema , sharedString ) { this . transaction = state . tr ; this . transaction . setMeta ( "fluid-local" , true ) ; this . things . push ( { type : "ether" , length : sharedString . getLength ( ) } ) ; } private splitAt ( position , offset ) { if ( position === 0 ) { return offset ; } const currentThing = this . things [ offset ] ; const newThing = { type : currentThing . type , event : currentThing . event , length : currentThing . length - position , annotations : currentThing . annotations , } ; currentThing . length = position ; this . things . splice ( offset + 1 , 0 , newThing ) ; return offset + 1 ; } private addRange ( range ) { let i = 0 ; let position = range . position ; for ( i = 0 ; i < this . things . length ; i ++ ) { if ( position < this . things [ i ] . length ) { break ; } position -= this . things [ i ] . length ; } switch ( range . operation ) { case MergeTreeDeltaType . REMOVE : { assert ( i < this . things . length , "Trying to insert removal node out-of-bounds!" ) ; i = this . splitAt ( position , i ) ; let length = range . segment . cachedLength ; while ( length > 0 ) { assert ( this . things [ i ] . type === "ether" , "Current thing does not have 'ether' node type!" ) ; if ( this . things [ i ] . length <= length ) { this . things [ i ] . type = "delete" ; this . things [ i ] . event = range ; length -= this . things [ i ] . length ; this . things [ i ] . length = 0 ; i ++ ; } else { this . splitAt ( length , i ) ; } } break ; } case MergeTreeDeltaType . INSERT : { const splicePoint = this . splitAt ( position , i ) ; this . things . splice ( splicePoint , 0 , { type : "insert" , event : range , length : range . segment . cachedLength } ) ; break ; } case MergeTreeDeltaType . ANNOTATE : { assert ( i < this . things . length , "Trying to insert annotations field on out-of-bounds node!" ) ; i = this . splitAt ( position , i ) ; let length = range . segment . cachedLength ; while ( length > 0 ) { assert ( this . things [ i ] . type === "ether" , "Current thing does not have 'ether' node type!" ) ; if ( this . things [ i ] . length <= length ) { this . things [ i ] . annotations = range . propertyDeltas ; this . things [ i ] . event = range ; length -= this . things [ i ] . length ; i ++ ; } else { this . splitAt ( length , i ) ; } } break ; } default : break ; } } public addSequencedDelta ( delta ) { for ( const range of delta . ranges ) { this . addRange ( range ) ; } } public build ( ) { console . log ( JSON . stringify ( this . things . map ( ( t ) => ( { type : t . type , length : t . length } ) ) , null , 2 ) ) ; let currentGroup ; const groups = new Array < IThingGroup > ( ) ; const annotations = [ ] ; let position = 0 ; for ( const thing of this . things ) { if ( thing . type === "ether" ) { if ( thing . annotations ) { annotations . push ( { from : position , to : position + thing . length , segment : thing . event ! . segment , propertyDeltas : thing . annotations , } ) ; } currentGroup = undefined ; position += thing . length ; } else { if ( ! currentGroup ) { currentGroup = { items : [ ] , position } ; groups . push ( currentGroup ) ; } currentGroup . items . push ( thing ) ; } } console . log ( `Total groups! ${ groups . length }` ) ; if ( groups . length === 1 ) { const group = groups [ 0 ] ; let removalSize = 0 ; const insertSegments = [ ] ; group . items . forEach ( ( value ) => { if ( value . type === "delete" ) { removalSize += value . event ! . segment . cachedLength ; } else { insertSegments . push ( value . event ! . segment ) ; } } ) ; const fragment = generateFragment ( insertSegments ) ; const slice = new Slice ( Fragment . fromJSON ( this . schema , fragment ) , this . getOpenStart ( fragment ) , this . getOpenEnd ( fragment ) ) ; this . transaction . replaceRange ( group . position , group . position + removalSize , slice ) ; } else if ( groups . length > 1 ) { const removalSizes = [ ] ; const insertSizes = [ ] ; const insertSegments = [ ] ; for ( const group of groups ) { let removalSize = 0 ; let groupSize = 0 ; group . items . forEach ( ( value ) => { if ( value . type === "delete" ) { removalSize += value . event ! . segment . cachedLength ; } else { insertSegments . push ( value . event ! . segment ) ; groupSize += value . event ! . segment . cachedLength ; } } ) ; removalSizes . push ( removalSize ) ; insertSizes . push ( groupSize ) ; } const fragment = generateFragment ( insertSegments ) ; const slice = new Slice ( Fragment . fromJSON ( this . schema , fragment ) , this . getOpenStart ( fragment ) , this . getOpenEnd ( fragment ) ) ; const gapSize = groups [ 1 ] . position - groups [ 0 ] . position ; this . transaction . step ( new ReplaceAroundStep ( groups [ 0 ] . position , groups [ 0 ] . position + removalSizes [ 0 ] + gapSize + removalSizes [ 1 ] , groups [ 0 ] . position + removalSizes [ 0 ] , groups [ 0 ] . position + removalSizes [ 0 ] + gapSize , slice , insertSizes [ 0 ] ) ) ; } for ( const annotation of annotations ) { const segment = annotation . segment ; for ( const prop of Object . keys ( annotation . propertyDeltas ) ) { const value = segment . properties ! [ prop ] ; if ( value ) { this . transaction . addMark ( annotation . from , annotation . to , this . schema . marks [ prop ] . create ( value ) ) ; } else { this . transaction . removeMark ( annotation . from , annotation . to , this . schema . marks [ prop ] ) ; } } } return this . transaction ; } private getOpenStart ( node ) { if ( ! node || node . length === 0 ) { return 0 ; } const start = node [ 0 ] ; return ! start . _open || ! start . content ? 0 : 1 + this . getOpenStart ( start . content ) ; } private getOpenEnd ( node ) { if ( ! node || node . length === 0 ) { return 0 ; } const end = node [ node . length - 1 ] ; return ! end . _open || ! end . content ? 0 : 1 + this . getOpenEnd ( end . content ) ; } } export function sliceToGroupOps ( from , slice , schema , insert ? , gapDistance ? ) { const ops = new Array < IMergeTreeDeltaOp > ( ) ; const sliceOpenStart = slice . openStart || 0 ; const sliceOpenEnd = slice . openEnd || 0 ; let offset = from + adjustOffset ( from , 0 , 0 , insert , gapDistance ) ; slice . content . forEach ( ( value , index ) => { const openStart = index === 0 ? sliceOpenStart - 1 : - 1 ; const openEnd = index === slice . content . length - 1 ? sliceOpenEnd - 1 : - 1 ; offset += sliceToGroupOpsInternal ( value , schema , openStart , openEnd , offset , ops , insert , gapDistance ) ; } ) ; return ops ; } function adjustOffset ( from , offset , value , insert , gapDistance ) { const newFrom = from + offset + value ; return newFrom === insert ? offset + value + gapDistance : offset + value ; } function sliceToGroupOpsInternal ( value , schema , openStart , openEnd , from , ops , insert ? , gapDistance ? ) { let offset = 0 ; let props ; if ( value . marks ) { props = { } ; for ( const mark of value . marks ) { props [ mark . type ] = mark . attrs || true ; } } const node = schema . nodes [ value . type ] ; if ( node . isInline ) { if ( value . type === "text" ) { const segment = new TextSegment ( value . text ) ; if ( props ) { segment . addProperties ( props ) ; } ops . push ( createInsertSegmentOp ( from + offset , segment ) ) ; offset = adjustOffset ( from , offset , value . text . length , insert , gapDistance ) ; } else { const nodeProps = { ... props , ... { type : value . type , attrs : value . attrs , } , } ; const marker = new Marker ( ReferenceType . Simple ) ; marker . addProperties ( nodeProps ) ; ops . push ( createInsertSegmentOp ( from + offset , marker ) ) ; offset = adjustOffset ( from , offset , 1 , insert , gapDistance ) ; } } else { if ( openStart < 0 ) { const beginProps = { ... props , ... { [ reservedRangeLabelsKey ] : [ proseMirrorTreeLabel ] , [ nodeTypeKey ] : value . type , } , } ; const marker = new Marker ( ReferenceType . NestBegin ) ; marker . addProperties ( beginProps ) ; ops . push ( createInsertSegmentOp ( from + offset , marker ) ) ; offset = adjustOffset ( from , offset , 1 , insert , gapDistance ) ; } if ( value . content ) { value . content . forEach ( ( content , index ) => { offset += sliceToGroupOpsInternal ( content , schema , index === 0 ? openStart - 1 : - 1 , index === value . content ! . length - 1 ? openEnd - 1 : - 1 , from + offset , ops , insert , gapDistance ) ; } ) ; } if ( openEnd < 0 ) { const endProps = { ... props , ... { [ reservedRangeLabelsKey ] : [ proseMirrorTreeLabel ] , [ nodeTypeKey ] : value . type , } , } ; const marker = new Marker ( ReferenceType . NestEnd ) ; marker . addProperties ( endProps ) ; ops . push ( createInsertSegmentOp ( from + offset , marker ) ) ; offset = adjustOffset ( from , offset , 1 , insert , gapDistance ) ; } } return offset ; } function generateFragment ( segments ) { const nodeStack = new Array < IProseMirrorNode > ( ) ; nodeStack . push ( { type : "doc" , content : [ ] } ) ; let openTop ; for ( const segment of segments ) { const top = nodeStack [ nodeStack . length - 1 ] ; if ( TextSegment . is ( segment ) ) { const nodeJson = { type : "text" , text : segment . text , } ; if ( segment . properties ) { nodeJson . marks = [ ] ; for ( const propertyKey of Object . keys ( segment . properties ) ) { nodeJson . marks . push ( { type : propertyKey , value : segment . properties [ propertyKey ] , } ) ; } } top . content ! . push ( nodeJson ) ; } else if ( Marker . is ( segment ) ) { const nodeType = segment . properties ! [ nodeTypeKey ] ; switch ( segment . refType ) { case ReferenceType . NestBegin : if ( openTop ) { top . content ! . push ( openTop ) ; openTop = undefined ; } const newNode = { type : nodeType , content : [ ] as IProseMirrorNode [ ] , _open : true } ; top . content ! . push ( newNode ) ; nodeStack . push ( newNode ) ; break ; case ReferenceType . NestEnd : if ( top . type === nodeType ) { top . _open = false ; nodeStack . pop ( ) ; } else { const newNode2 = { type : nodeType , content : [ ] as IProseMirrorNode [ ] , _open : true } ; if ( openTop ) { newNode2 . content . push ( openTop ) ; } openTop = newNode2 ; } break ; case ReferenceType . Simple : const nodeJson = { type : segment . properties ! . type , attrs : segment . properties ! . attrs , } ; if ( segment . properties ) { nodeJson . marks = [ ] ; for ( const propertyKey of Object . keys ( segment . properties ) ) { if ( propertyKey !== "type" && propertyKey !== "attrs" ) { nodeJson . marks . push ( { type : propertyKey , value : segment . properties [ propertyKey ] , } ) ; } } } top . content ! . push ( nodeJson ) ; break ; default : throw new Error ( "Unknown marker" ) ; } } } const doc = nodeStack [ 0 ] ; const content = doc . content ! ; return content ; }