506 lines
15 KiB
JavaScript
506 lines
15 KiB
JavaScript
import ItemString from '../Struct/ItemString.js'
|
|
import ItemEmbed from '../Struct/ItemEmbed.js'
|
|
import ItemFormat from '../Struct/ItemFormat.js'
|
|
import { logID } from '../MessageHandler/messageToString.js'
|
|
import { YArrayEvent, default as YArray } from './YArray.js'
|
|
|
|
function integrateItem (item, parent, y, left, right) {
|
|
item._origin = left
|
|
item._left = left
|
|
item._right = right
|
|
item._right_origin = right
|
|
item._parent = parent
|
|
if (y !== null) {
|
|
item._integrate(y)
|
|
} else if (left === null) {
|
|
parent._start = item
|
|
} else {
|
|
left._right = item
|
|
}
|
|
}
|
|
|
|
function findNextPosition (currentAttributes, parent, left, right, count) {
|
|
while (right !== null && count > 0) {
|
|
switch (right.constructor) {
|
|
case ItemEmbed:
|
|
case ItemString:
|
|
const rightLen = right._deleted ? 0 : (right._length - 1)
|
|
if (count <= rightLen) {
|
|
right = right._splitAt(parent._y, count)
|
|
left = right._left
|
|
return [left, right, currentAttributes]
|
|
}
|
|
if (right._deleted === false) {
|
|
count -= right._length
|
|
}
|
|
break
|
|
case ItemFormat:
|
|
if (right._deleted === false) {
|
|
updateCurrentAttributes(currentAttributes, right)
|
|
}
|
|
break
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
return [left, right, currentAttributes]
|
|
}
|
|
|
|
function findPosition (parent, pos) {
|
|
let currentAttributes = new Map()
|
|
let left = null
|
|
let right = parent._start
|
|
return findNextPosition(currentAttributes, parent, left, right, pos)
|
|
}
|
|
|
|
// negate applied formats
|
|
function insertNegatedAttributes (y, parent, left, right, negatedAttributes) {
|
|
// check if we really need to remove attributes
|
|
while (
|
|
right !== null && (
|
|
right._deleted === true || (
|
|
right.constructor === ItemFormat &&
|
|
(negatedAttributes.get(right.key) === right.value)
|
|
)
|
|
)
|
|
) {
|
|
if (right._deleted === false) {
|
|
negatedAttributes.delete(right.key)
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
for (let [key, val] of negatedAttributes) {
|
|
let format = new ItemFormat()
|
|
format.key = key
|
|
format.value = val
|
|
integrateItem(format, parent, y, left, right)
|
|
left = format
|
|
}
|
|
return [left, right]
|
|
}
|
|
|
|
function updateCurrentAttributes (currentAttributes, item) {
|
|
const value = item.value
|
|
const key = item.key
|
|
if (value === null) {
|
|
currentAttributes.delete(key)
|
|
} else {
|
|
currentAttributes.set(key, value)
|
|
}
|
|
}
|
|
|
|
function minimizeAttributeChanges (left, right, currentAttributes, attributes) {
|
|
// go right while attributes[right.key] === right.value (or right is deleted)
|
|
while (true) {
|
|
if (right === null) {
|
|
break
|
|
} else if (right._deleted === true) {
|
|
// continue
|
|
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
|
|
// found a format, update currentAttributes and continue
|
|
updateCurrentAttributes(currentAttributes, right)
|
|
} else {
|
|
break
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
return [left, right]
|
|
}
|
|
|
|
function insertText (y, text, parent, left, right, currentAttributes, attributes) {
|
|
for (let [key] of currentAttributes) {
|
|
if (attributes.hasOwnProperty(key) === false) {
|
|
attributes[key] = null
|
|
}
|
|
}
|
|
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
|
let negatedAttributes = new Map()
|
|
// insert format-start items
|
|
for (let key in attributes) {
|
|
const val = attributes[key]
|
|
const currentVal = currentAttributes.get(key)
|
|
if (currentVal !== val) {
|
|
// save negated attribute (set null if currentVal undefined)
|
|
negatedAttributes.set(key, currentVal || null)
|
|
let format = new ItemFormat()
|
|
format.key = key
|
|
format.value = val
|
|
integrateItem(format, parent, y, left, right)
|
|
left = format
|
|
}
|
|
}
|
|
// insert content
|
|
let item
|
|
if (text.constructor === String) {
|
|
item = new ItemString()
|
|
item._content = text
|
|
} else {
|
|
item = new ItemEmbed()
|
|
item.embed = text
|
|
}
|
|
integrateItem(item, parent, y, left, right)
|
|
left = item
|
|
return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
|
|
}
|
|
|
|
function formatText (y, length, parent, left, right, currentAttributes, attributes) {
|
|
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
|
let negatedAttributes = new Map()
|
|
// insert format-start items
|
|
for (let key in attributes) {
|
|
const val = attributes[key]
|
|
const currentVal = currentAttributes.get(key)
|
|
if (currentVal !== val) {
|
|
// save negated attribute (set null if currentVal undefined)
|
|
negatedAttributes.set(key, currentVal || null)
|
|
let format = new ItemFormat()
|
|
format.key = key
|
|
format.value = val
|
|
integrateItem(format, parent, y, left, right)
|
|
left = format
|
|
}
|
|
}
|
|
// iterate until first non-format or null is found
|
|
// delete all formats with attributes[format.key] != null
|
|
while (length > 0 && right !== null) {
|
|
if (right._deleted === false) {
|
|
switch (right.constructor) {
|
|
case ItemFormat:
|
|
if (attributes.hasOwnProperty(right.key)) {
|
|
if (attributes[right.key] === right.value) {
|
|
negatedAttributes.delete(right.key)
|
|
} else {
|
|
negatedAttributes.set(right.key, right.value)
|
|
}
|
|
right._delete(y)
|
|
}
|
|
updateCurrentAttributes(currentAttributes, right)
|
|
break
|
|
case ItemEmbed:
|
|
case ItemString:
|
|
right._splitAt(y, length)
|
|
length -= right._length
|
|
break
|
|
}
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
|
|
}
|
|
|
|
function deleteText (y, length, parent, left, right, currentAttributes) {
|
|
while (length > 0 && right !== null) {
|
|
if (right._deleted === false) {
|
|
switch (right.constructor) {
|
|
case ItemFormat:
|
|
updateCurrentAttributes(currentAttributes, right)
|
|
break
|
|
case ItemEmbed:
|
|
case ItemString:
|
|
right._splitAt(y, length)
|
|
length -= right._length
|
|
right._delete(y)
|
|
break
|
|
}
|
|
}
|
|
left = right
|
|
right = right._right
|
|
}
|
|
return [left, right]
|
|
}
|
|
|
|
class YTextEvent extends YArrayEvent {
|
|
constructor (ytext, remote, transaction) {
|
|
super(ytext, remote, transaction)
|
|
this._delta = null
|
|
}
|
|
get delta () {
|
|
if (this._delta === null) {
|
|
const y = this.target._y
|
|
y.transact(() => {
|
|
let item = this.target._start
|
|
const delta = []
|
|
const added = this.addedElements
|
|
const removed = this.removedElements
|
|
this._delta = delta
|
|
let action = null
|
|
let attributes = {} // counts added or removed new attributes for retain
|
|
const currentAttributes = new Map() // saves all current attributes for insert
|
|
const oldAttributes = new Map()
|
|
let insert = ''
|
|
let retain = 0
|
|
let deleteLen = 0
|
|
const addOp = function addOp () {
|
|
if (action !== null) {
|
|
let op
|
|
switch (action) {
|
|
case 'delete':
|
|
op = { delete: deleteLen }
|
|
deleteLen = 0
|
|
break
|
|
case 'insert':
|
|
op = { insert }
|
|
if (currentAttributes.size > 0) {
|
|
op.attributes = {}
|
|
for (let [key, value] of currentAttributes) {
|
|
if (value !== null) {
|
|
op.attributes[key] = value
|
|
}
|
|
}
|
|
}
|
|
insert = ''
|
|
break
|
|
case 'retain':
|
|
op = { retain }
|
|
if (Object.keys(attributes).length > 0) {
|
|
op.attributes = {}
|
|
for (let key in attributes) {
|
|
op.attributes[key] = attributes[key]
|
|
}
|
|
}
|
|
retain = 0
|
|
break
|
|
}
|
|
delta.push(op)
|
|
action = null
|
|
}
|
|
}
|
|
while (item !== null) {
|
|
switch (item.constructor) {
|
|
case ItemEmbed:
|
|
if (added.has(item)) {
|
|
addOp()
|
|
action = 'insert'
|
|
insert = item.embed
|
|
addOp()
|
|
} else if (removed.has(item)) {
|
|
if (action !== 'delete') {
|
|
addOp()
|
|
action = 'delete'
|
|
}
|
|
deleteLen += 1
|
|
} else if (item._deleted === false) {
|
|
if (action !== 'retain') {
|
|
addOp()
|
|
action = 'retain'
|
|
}
|
|
retain += 1
|
|
}
|
|
break
|
|
case ItemString:
|
|
if (added.has(item)) {
|
|
if (action !== 'insert') {
|
|
addOp()
|
|
action = 'insert'
|
|
}
|
|
insert += item._content
|
|
} else if (removed.has(item)) {
|
|
if (action !== 'delete') {
|
|
addOp()
|
|
action = 'delete'
|
|
}
|
|
deleteLen += item._length
|
|
} else if (item._deleted === false) {
|
|
if (action !== 'retain') {
|
|
addOp()
|
|
action = 'retain'
|
|
}
|
|
retain += item._length
|
|
}
|
|
break
|
|
case ItemFormat:
|
|
if (added.has(item)) {
|
|
const curVal = currentAttributes.get(item.key) || null
|
|
if (curVal !== item.value) {
|
|
if (action === 'retain') {
|
|
addOp()
|
|
}
|
|
if (item.value === (oldAttributes.get(item.key) || null)) {
|
|
delete attributes[item.key]
|
|
} else {
|
|
attributes[item.key] = item.value
|
|
}
|
|
} else {
|
|
item._delete(y)
|
|
}
|
|
} else if (removed.has(item)) {
|
|
oldAttributes.set(item.key, item.value)
|
|
const curVal = currentAttributes.get(item.key) || null
|
|
if (curVal !== item.value) {
|
|
if (action === 'retain') {
|
|
addOp()
|
|
}
|
|
attributes[item.key] = curVal
|
|
}
|
|
} else if (item._deleted === false) {
|
|
oldAttributes.set(item.key, item.value)
|
|
if (attributes.hasOwnProperty(item.key)) {
|
|
if (attributes[item.key] !== item.value) {
|
|
if (action === 'retain') {
|
|
addOp()
|
|
}
|
|
if (item.value === null) {
|
|
attributes[item.key] = item.value
|
|
} else {
|
|
delete attributes[item.key]
|
|
}
|
|
} else {
|
|
item._delete(y)
|
|
}
|
|
}
|
|
}
|
|
if (item._deleted === false) {
|
|
if (action === 'insert') {
|
|
addOp()
|
|
}
|
|
updateCurrentAttributes(currentAttributes, item)
|
|
}
|
|
break
|
|
}
|
|
item = item._right
|
|
}
|
|
addOp()
|
|
while (this._delta.length > 0) {
|
|
let lastOp = this._delta[this._delta.length - 1]
|
|
if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) {
|
|
// retain delta's if they don't assign attributes
|
|
this._delta.pop()
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return this._delta
|
|
}
|
|
}
|
|
|
|
export default class YText extends YArray {
|
|
constructor (string) {
|
|
super()
|
|
if (typeof string === 'string') {
|
|
const start = new ItemString()
|
|
start._parent = this
|
|
start._content = string
|
|
this._start = start
|
|
}
|
|
}
|
|
_callObserver (transaction, parentSubs, remote) {
|
|
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
|
}
|
|
toString () {
|
|
let str = ''
|
|
let n = this._start
|
|
while (n !== null) {
|
|
if (!n._deleted && n._countable) {
|
|
str += n._content
|
|
}
|
|
n = n._right
|
|
}
|
|
return str
|
|
}
|
|
applyDelta (delta) {
|
|
this._transact(y => {
|
|
let left = null
|
|
let right = this._start
|
|
const currentAttributes = new Map()
|
|
for (let i = 0; i < delta.length; i++) {
|
|
let op = delta[i]
|
|
if (op.hasOwnProperty('insert')) {
|
|
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
|
|
} else if (op.hasOwnProperty('retain')) {
|
|
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
|
|
} else if (op.hasOwnProperty('delete')) {
|
|
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
/**
|
|
* As defined by Quilljs - https://quilljs.com/docs/delta/
|
|
*/
|
|
toDelta () {
|
|
let ops = []
|
|
let currentAttributes = new Map()
|
|
let str = ''
|
|
let n = this._start
|
|
function packStr () {
|
|
if (str.length > 0) {
|
|
// pack str with attributes to ops
|
|
let attributes = {}
|
|
let addAttributes = false
|
|
for (let [key, value] of currentAttributes) {
|
|
addAttributes = true
|
|
attributes[key] = value
|
|
}
|
|
let op = { insert: str }
|
|
if (addAttributes) {
|
|
op.attributes = attributes
|
|
}
|
|
ops.push(op)
|
|
str = ''
|
|
}
|
|
}
|
|
while (n !== null) {
|
|
if (!n._deleted) {
|
|
switch (n.constructor) {
|
|
case ItemString:
|
|
str += n._content
|
|
break
|
|
case ItemFormat:
|
|
packStr()
|
|
updateCurrentAttributes(currentAttributes, n)
|
|
break
|
|
}
|
|
}
|
|
n = n._right
|
|
}
|
|
packStr()
|
|
return ops
|
|
}
|
|
insert (pos, text, attributes = {}) {
|
|
if (text.length <= 0) {
|
|
return
|
|
}
|
|
this._transact(y => {
|
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
|
insertText(y, text, this, left, right, currentAttributes, attributes)
|
|
})
|
|
}
|
|
insertEmbed (pos, embed, attributes = {}) {
|
|
if (embed.constructor !== Object) {
|
|
throw new Error('Embed must be an Object')
|
|
}
|
|
this._transact(y => {
|
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
|
insertText(y, embed, this, left, right, currentAttributes, attributes)
|
|
})
|
|
}
|
|
delete (pos, length) {
|
|
if (length === 0) {
|
|
return
|
|
}
|
|
this._transact(y => {
|
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
|
deleteText(y, length, this, left, right, currentAttributes)
|
|
})
|
|
}
|
|
format (pos, length, attributes) {
|
|
this._transact(y => {
|
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
|
if (right === null) {
|
|
return
|
|
}
|
|
formatText(y, length, this, left, right, currentAttributes, attributes)
|
|
})
|
|
}
|
|
_logString () {
|
|
const left = this._left !== null ? this._left._lastId : null
|
|
const origin = this._origin !== null ? this._origin._lastId : null
|
|
return `YText(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
}
|
|
}
|