2020-05-06 06:08:21 -04:00
/ *
* decaffeinate suggestions :
* DS101 : Remove unnecessary use of Array . from
* DS102 : Remove unnecessary code created because of implicit returns
* DS205 : Consider reworking code to avoid use of IIFEs
* DS207 : Consider shorter variations of null checks
* Full docs : https : //github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
* /
// An alternate composable implementation for text. This is much closer
// to the implementation used by google wave.
//
// Ops are lists of components which iterate over the whole document.
// Components are either:
// A number N: Skip N characters in the original document
// {i:'str'}: Insert 'str' at the current position in the document
// {d:'str'}: Delete 'str', which appears at the current position in the document
//
// Eg: [3, {i:'hi'}, 5, {d:'internet'}]
//
// Snapshots are strings.
let makeAppend ;
const p = function ( ) { } ; //require('util').debug
const i = function ( ) { } ; //require('util').inspect
const exports = ( typeof WEB !== 'undefined' && WEB !== null ) ? { } : module . exports ;
exports . name = 'text-composable' ;
exports . create = ( ) => '' ;
// -------- Utility methods
const checkOp = function ( op ) {
if ( ! Array . isArray ( op ) ) { throw new Error ( 'Op must be an array of components' ) ; }
let last = null ;
return ( ( ) => {
const result = [ ] ;
for ( let c of Array . from ( op ) ) {
if ( typeof ( c ) === 'object' ) {
if ( ( ( c . i == null ) || ! ( c . i . length > 0 ) ) && ( ( c . d == null ) || ! ( c . d . length > 0 ) ) ) { throw new Error ( ` Invalid op component: ${ i ( c ) } ` ) ; }
} else {
if ( typeof ( c ) !== 'number' ) { throw new Error ( 'Op components must be objects or numbers' ) ; }
if ( ! ( c > 0 ) ) { throw new Error ( 'Skip components must be a positive number' ) ; }
if ( typeof ( last ) === 'number' ) { throw new Error ( 'Adjacent skip components should be added' ) ; }
}
result . push ( last = c ) ;
}
return result ;
} ) ( ) ;
} ;
// Makes a function for appending components to a given op.
// Exported for the randomOpGenerator.
exports . _makeAppend = ( makeAppend = op => ( function ( component ) {
if ( ( component === 0 ) || ( component . i === '' ) || ( component . d === '' ) ) {
return ;
} else if ( op . length === 0 ) {
return op . push ( component ) ;
} else if ( ( typeof ( component ) === 'number' ) && ( typeof ( op [ op . length - 1 ] ) === 'number' ) ) {
return op [ op . length - 1 ] += component ;
} else if ( ( component . i != null ) && ( op [ op . length - 1 ] . i != null ) ) {
return op [ op . length - 1 ] . i += component . i ;
} else if ( ( component . d != null ) && ( op [ op . length - 1 ] . d != null ) ) {
return op [ op . length - 1 ] . d += component . d ;
} else {
return op . push ( component ) ;
}
} ) ) ;
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
// checkOp op
// Makes 2 functions for taking components from the start of an op, and for peeking
// at the next op that could be taken.
const makeTake = function ( op ) {
// The index of the next component to take
let idx = 0 ;
// The offset into the component
let offset = 0 ;
// Take up to length n from the front of op. If n is null, take the next
// op component. If indivisableField == 'd', delete components won't be separated.
// If indivisableField == 'i', insert components won't be separated.
const take = function ( n , indivisableField ) {
let c ;
if ( idx === op . length ) { return null ; }
//assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
if ( typeof ( op [ idx ] ) === 'number' ) {
if ( ( n == null ) || ( ( op [ idx ] - offset ) <= n ) ) {
c = op [ idx ] - offset ;
++ idx ; offset = 0 ;
return c ;
} else {
offset += n ;
return n ;
}
} else {
// Take from the string
const field = op [ idx ] . i ? 'i' : 'd' ;
c = { } ;
if ( ( n == null ) || ( ( op [ idx ] [ field ] . length - offset ) <= n ) || ( field === indivisableField ) ) {
c [ field ] = op [ idx ] [ field ] . slice ( offset ) ;
++ idx ; offset = 0 ;
} else {
c [ field ] = op [ idx ] [ field ] . slice ( offset , ( offset + n ) ) ;
offset += n ;
}
return c ;
}
} ;
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
const peekType = ( ) => op [ idx ] ;
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
return [ take , peekType ] ;
} ;
// Find and return the length of an op component
const componentLength = function ( component ) {
if ( typeof ( component ) === 'number' ) {
return component ;
} else if ( component . i != null ) {
return component . i . length ;
} else {
return component . d . length ;
}
} ;
// Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
// adjacent inserts and deletes.
exports . normalize = function ( op ) {
const newOp = [ ] ;
const append = makeAppend ( newOp ) ;
for ( let component of Array . from ( op ) ) { append ( component ) ; }
return newOp ;
} ;
// Apply the op to the string. Returns the new string.
exports . apply = function ( str , op ) {
p ( ` Applying ${ i ( op ) } to ' ${ str } ' ` ) ;
if ( typeof ( str ) !== 'string' ) { throw new Error ( 'Snapshot should be a string' ) ; }
checkOp ( op ) ;
const pos = 0 ;
const newDoc = [ ] ;
for ( let component of Array . from ( op ) ) {
if ( typeof ( component ) === 'number' ) {
if ( component > str . length ) { throw new Error ( 'The op is too long for this document' ) ; }
newDoc . push ( str . slice ( 0 , component ) ) ;
str = str . slice ( component ) ;
} else if ( component . i != null ) {
newDoc . push ( component . i ) ;
} else {
if ( component . d !== str . slice ( 0 , component . d . length ) ) { throw new Error ( ` The deleted text ' ${ component . d } ' doesn't match the next characters in the document ' ${ str . slice ( 0 , component . d . length ) } ' ` ) ; }
str = str . slice ( component . d . length ) ;
}
}
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
if ( '' !== str ) { throw new Error ( "The applied op doesn't traverse the entire document" ) ; }
return newDoc . join ( '' ) ;
} ;
// transform op1 by op2. Return transformed version of op1.
// op1 and op2 are unchanged by transform.
exports . transform = function ( op , otherOp , side ) {
let component ;
if ( ( side !== 'left' ) && ( side !== 'right' ) ) { throw new Error ( ` side ( ${ side } must be 'left' or 'right' ` ) ; }
checkOp ( op ) ;
checkOp ( otherOp ) ;
const newOp = [ ] ;
const append = makeAppend ( newOp ) ;
const [ take , peek ] = Array . from ( makeTake ( op ) ) ;
for ( component of Array . from ( otherOp ) ) {
var chunk , length ;
if ( typeof ( component ) === 'number' ) { // Skip
length = component ;
while ( length > 0 ) {
chunk = take ( length , 'i' ) ;
if ( chunk === null ) { throw new Error ( 'The op traverses more elements than the document has' ) ; }
append ( chunk ) ;
if ( ( typeof ( chunk ) !== 'object' ) || ( chunk . i == null ) ) { length -= componentLength ( chunk ) ; }
}
} else if ( component . i != null ) { // Insert
if ( side === 'left' ) {
// The left insert should go first.
const o = peek ( ) ;
if ( o != null ? o . i : undefined ) { append ( take ( ) ) ; }
}
// Otherwise, skip the inserted text.
append ( component . i . length ) ;
} else { // Delete.
//assert.ok component.d
( {
length
} = component . d ) ;
while ( length > 0 ) {
chunk = take ( length , 'i' ) ;
if ( chunk === null ) { throw new Error ( 'The op traverses more elements than the document has' ) ; }
if ( typeof ( chunk ) === 'number' ) {
length -= chunk ;
} else if ( chunk . i != null ) {
append ( chunk ) ;
} else {
//assert.ok chunk.d
// The delete is unnecessary now.
length -= chunk . d . length ;
}
}
}
}
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
// Append extras from op1
while ( component = take ( ) ) {
if ( ( component != null ? component . i : undefined ) == null ) { throw new Error ( ` Remaining fragments in the op: ${ i ( component ) } ` ) ; }
append ( component ) ;
}
return newOp ;
} ;
// Compose 2 ops into 1 op.
exports . compose = function ( op1 , op2 ) {
let component ;
p ( ` COMPOSE ${ i ( op1 ) } + ${ i ( op2 ) } ` ) ;
checkOp ( op1 ) ;
checkOp ( op2 ) ;
const result = [ ] ;
const append = makeAppend ( result ) ;
const [ take , _ ] = Array . from ( makeTake ( op1 ) ) ;
for ( component of Array . from ( op2 ) ) {
var chunk , length ;
if ( typeof ( component ) === 'number' ) { // Skip
length = component ;
while ( length > 0 ) {
chunk = take ( length , 'd' ) ;
if ( chunk === null ) { throw new Error ( 'The op traverses more elements than the document has' ) ; }
append ( chunk ) ;
if ( ( typeof ( chunk ) !== 'object' ) || ( chunk . d == null ) ) { length -= componentLength ( chunk ) ; }
}
} else if ( component . i != null ) { // Insert
append ( { i : component . i } ) ;
} else { // Delete
let offset = 0 ;
while ( offset < component . d . length ) {
chunk = take ( component . d . length - offset , 'd' ) ;
if ( chunk === null ) { throw new Error ( 'The op traverses more elements than the document has' ) ; }
// If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
if ( typeof ( chunk ) === 'number' ) {
append ( { d : component . d . slice ( offset , ( offset + chunk ) ) } ) ;
offset += chunk ;
} else if ( chunk . i != null ) {
if ( component . d . slice ( offset , ( offset + chunk . i . length ) ) !== chunk . i ) { throw new Error ( "The deleted text doesn't match the inserted text" ) ; }
offset += chunk . i . length ;
// The ops cancel each other out.
} else {
// Delete
append ( chunk ) ;
}
}
}
}
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
// Append extras from op1
while ( component = take ( ) ) {
if ( ( component != null ? component . d : undefined ) == null ) { throw new Error ( ` Trailing stuff in op1 ${ i ( component ) } ` ) ; }
append ( component ) ;
}
return result ;
} ;
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
const invertComponent = function ( c ) {
if ( typeof ( c ) === 'number' ) {
return c ;
} else if ( c . i != null ) {
return { d : c . i } ;
} else {
return { i : c . d } ;
}
} ;
// Invert an op
exports . invert = function ( op ) {
const result = [ ] ;
const append = makeAppend ( result ) ;
for ( let component of Array . from ( op ) ) { append ( invertComponent ( component ) ) ; }
2014-02-12 05:40:42 -05:00
2020-05-06 06:08:21 -04:00
return result ;
} ;
if ( typeof window !== 'undefined' && window !== null ) {
if ( ! window . ot ) { window . ot = { } ; }
if ( ! window . ot . types ) { window . ot . types = { } ; }
window . ot . types . text = exports ;
}
2014-02-12 05:40:42 -05:00