quotations: fix splitting quoted item and inserting new item in quoted range
This commit is contained in:
parent
94c8ee3a87
commit
8788a4b9e0
@ -24,7 +24,8 @@ import {
|
||||
addChangedTypeToTransaction,
|
||||
isDeleted,
|
||||
StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction, // eslint-disable-line
|
||||
YWeakLink
|
||||
YWeakLink,
|
||||
joinLinkedRange
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
@ -106,6 +107,14 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
if (leftItem.redone !== null) {
|
||||
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
|
||||
}
|
||||
if (leftItem.linked) {
|
||||
rightItem.linked = true
|
||||
const allLinks = transaction.doc.store.linkedBy
|
||||
const linkedBy = allLinks.get(leftItem)
|
||||
if (linkedBy !== undefined) {
|
||||
allLinks.set(rightItem, new Set(linkedBy))
|
||||
}
|
||||
}
|
||||
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
|
||||
leftItem.right = rightItem
|
||||
// update right
|
||||
@ -554,9 +563,15 @@ export class Item extends AbstractStruct {
|
||||
this.left.delete(transaction)
|
||||
}
|
||||
}
|
||||
// adjust length of parent
|
||||
if (this.parentSub === null && this.countable && !this.deleted) {
|
||||
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
||||
if (this.parentSub === null && !this.deleted) {
|
||||
if (this.countable) {
|
||||
// adjust length of parent
|
||||
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
||||
}
|
||||
if (this.left && this.left.linked && this.right && this.right.linked) {
|
||||
// this item exists within a quoted range
|
||||
joinLinkedRange(transaction, this)
|
||||
}
|
||||
}
|
||||
addStruct(transaction.doc.store, this)
|
||||
this.content.integrate(transaction, this)
|
||||
|
@ -212,7 +212,7 @@ export class YArray extends AbstractType {
|
||||
* @param {number} length The number of elements to include in returned weak link reference.
|
||||
* @return {YWeakLink<T>}
|
||||
*/
|
||||
quote(index, length = 1) {
|
||||
quote (index, length = 1) {
|
||||
if (this.doc !== null) {
|
||||
return transact(this.doc, transaction => {
|
||||
return arrayWeakLink(transaction, this, index, length)
|
||||
|
@ -241,7 +241,7 @@ export class YMap extends AbstractType {
|
||||
* @param {string} key
|
||||
* @return {YWeakLink<MapType>|undefined}
|
||||
*/
|
||||
link(key) {
|
||||
link (key) {
|
||||
return mapWeakLink(this, key)
|
||||
}
|
||||
|
||||
|
@ -27,12 +27,14 @@ import {
|
||||
typeMapGetAll,
|
||||
updateMarkerChanges,
|
||||
ContentType,
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, // eslint-disable-line
|
||||
quoteText
|
||||
} from '../internals.js'
|
||||
|
||||
import * as object from 'lib0/object'
|
||||
import * as map from 'lib0/map'
|
||||
import * as error from 'lib0/error'
|
||||
import { WeakLink } from 'yjs'
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
@ -1155,6 +1157,31 @@ export class YText extends AbstractType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a WeakLink representing a dynamic quotation of a range of elements.
|
||||
*
|
||||
* In case when quotation happens in a middle of formatting range, formatting
|
||||
* attributes will be split into before|within|after eg. quoting fragment of
|
||||
* `<i>hello world</i>` could result in `<i>he</i>"<i>llo wo</i>"<i>rld</i>`
|
||||
* where `"<i>llo wo</i>"` represents quoted range.
|
||||
*
|
||||
* @param {number} index The index where quoted range should start
|
||||
* @param {number} length Number of quoted elements
|
||||
* @return {WeakLink<string>}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
quote (index, length) {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
return transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index)
|
||||
return quoteText(transaction, this, pos, length)
|
||||
})
|
||||
}
|
||||
throw new Error('cannot quote YText which has not been integrated into any Doc')
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes text starting from an index.
|
||||
*
|
||||
|
@ -11,7 +11,9 @@ import {
|
||||
YWeakLinkRefID,
|
||||
writeID,
|
||||
readID,
|
||||
RelativePosition
|
||||
RelativePosition,
|
||||
ItemTextListPosition,
|
||||
ContentString
|
||||
} from "../internals.js"
|
||||
|
||||
/**
|
||||
@ -53,7 +55,7 @@ export class YWeakLink extends AbstractType {
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSingle() {
|
||||
isSingle () {
|
||||
return this._quoteStart.item === this._quoteEnd.item
|
||||
}
|
||||
|
||||
@ -62,7 +64,7 @@ export class YWeakLink extends AbstractType {
|
||||
*
|
||||
* @return {T|undefined}
|
||||
*/
|
||||
deref() {
|
||||
deref () {
|
||||
if (this._firstItem !== null) {
|
||||
let item = this._firstItem
|
||||
if (item.parentSub !== null) {
|
||||
@ -85,7 +87,7 @@ export class YWeakLink extends AbstractType {
|
||||
*
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
unqote() {
|
||||
unqote () {
|
||||
let result = /** @type {Array<any>} */ ([])
|
||||
let item = this._firstItem
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
@ -182,6 +184,31 @@ export class YWeakLink extends AbstractType {
|
||||
writeID(encoder.restEncoder, /** @type {ID} */ (this._quoteEnd.item))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unformatted string representation of this quoted text range.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
toString () {
|
||||
let str = ''
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._firstItem
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
}
|
||||
const lastId = n.lastId
|
||||
if (lastId.client === end.client && lastId.clock === end.clock) {
|
||||
break;
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -259,6 +286,50 @@ export const arrayWeakLink = (transaction, parent, index, length = 1) => {
|
||||
throw invalidQuotedRange
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {WeakLink} to an YMap element at given key.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {ItemTextListPosition} pos
|
||||
* @param {number} length
|
||||
* @return {YWeakLink<string>}
|
||||
*/
|
||||
export const quoteText = (transaction, parent, pos, length) => {
|
||||
if (pos.right !== null) {
|
||||
const startItem = pos.right
|
||||
const endIndex = pos.index + length
|
||||
while (pos.index < endIndex) {
|
||||
pos.forward()
|
||||
}
|
||||
if (pos.left !== null) {
|
||||
let endItem = pos.left
|
||||
if (pos.index > endIndex) {
|
||||
const overflow = pos.index - endIndex
|
||||
endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + endItem.length - overflow - 1))
|
||||
}
|
||||
const start = new RelativePosition(null, null, startItem.id, 0)
|
||||
const end = new RelativePosition(null, null, endItem.lastId, -1)
|
||||
const link = new YWeakLink(start, end, startItem)
|
||||
if (parent.doc !== null) {
|
||||
transact(parent.doc, (transaction) => {
|
||||
const end = /** @type {ID} */ (link._quoteEnd.item)
|
||||
for (let item = link._firstItem; item !== null; item = item = item.right) {
|
||||
createLink(transaction, item, link)
|
||||
const lastId = item.lastId
|
||||
if (lastId.client === end.client && lastId.clock === end.clock) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
throw invalidQuotedRange
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {WeakLink} to an YMap element at given key.
|
||||
*
|
||||
@ -319,4 +390,29 @@ export const unlinkFrom = (transaction, source, linkRef) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebinds linkedBy links pointed between neighbours of a current item.
|
||||
* This method expects that current item has both left and right neighbours.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
export const joinLinkedRange = (transaction, item) => {
|
||||
item.linked = true
|
||||
const allLinks = transaction.doc.store.linkedBy
|
||||
const leftLinks = allLinks.get(/** @type {Item} */ (item.left))
|
||||
const rightLinks = allLinks.get(/** @type {Item} */ (item.right))
|
||||
if (leftLinks && rightLinks) {
|
||||
const common = new Set()
|
||||
for (let link of leftLinks) {
|
||||
if (rightLinks.has(link)) {
|
||||
common.add(link)
|
||||
}
|
||||
}
|
||||
if (common.size != 0) {
|
||||
allLinks.set(item, common)
|
||||
}
|
||||
}
|
||||
}
|
@ -472,6 +472,75 @@ export const testDeepObserveArray = tc => { //FIXME
|
||||
t.compare(events.length, 1)
|
||||
t.compare(events[0].target, link)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testDeepObserveNewElementWithinQuotedRange = tc => {
|
||||
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||
const m1 = new Y.Map()
|
||||
const m3 = new Y.Map()
|
||||
array0.insert(0, [1,m1,m3,2])
|
||||
const link0 = array0.quote(1, 2)
|
||||
array0.insert(0, [link0])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
let e0 = []
|
||||
link0.observeDeep((evts) => {
|
||||
e0 = []
|
||||
for (let e of evts) {
|
||||
switch (e.constructor) {
|
||||
case Y.YMapEvent:
|
||||
e0.push({target: e.target, keys: e.keys})
|
||||
break;
|
||||
case Y.YWeakLinkEvent:
|
||||
e0.push({target: e.target})
|
||||
break;
|
||||
default: throw new Error('unexpected event type ' + e.constructor)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const link1 = /** @type {Y.WeakLink<any>} */ (array1.get(0))
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
let e1 = []
|
||||
link1.observeDeep((evts) => {
|
||||
e1 = []
|
||||
for (let e of evts) {
|
||||
switch (e.constructor) {
|
||||
case Y.YMapEvent:
|
||||
e1.push({target: e.target, keys: e.keys})
|
||||
break;
|
||||
case Y.YWeakLinkEvent:
|
||||
e1.push({target: e.target})
|
||||
break;
|
||||
default: throw new Error('unexpected event type ' + e.constructor)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const m20 = new Y.Map()
|
||||
array0.insert(3, [m20])
|
||||
|
||||
m20.set('key', 'value')
|
||||
t.compare(e0.length, 1)
|
||||
t.compare(e0[0].target, m20)
|
||||
t.compare(e0[0].keys, new Map([['key', {action:'add', oldValue: undefined}]]))
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const m21 = array1.get(3)
|
||||
t.compare(e1.length, 1)
|
||||
t.compare(e1[0].target, m21)
|
||||
t.compare(e1[0].keys, new Map([['key', {action:'add', oldValue: undefined}]]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@ -608,4 +677,56 @@ export const testRemoteMapUpdate = tc => {
|
||||
t.compare(link0.deref(), 3)
|
||||
t.compare(link1.deref(), 3)
|
||||
t.compare(link2.deref(), 3)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testTextBasic = tc => {
|
||||
const { testConnector, text0, array0, text1 } = init(tc, { users: 2 })
|
||||
|
||||
text0.insert(0, 'abcd')
|
||||
const link0 = text0.quote(1, 2)
|
||||
t.compare(link0.toString(), 'bc')
|
||||
text0.insert(2, 'ef')
|
||||
t.compare(link0.toString(), 'befc')
|
||||
text0.delete(3, 3)
|
||||
t.compare(link0.toString(), 'be')
|
||||
text0.insertEmbed(3, link0)
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const delta = text1.toDelta()
|
||||
const { insert } = delta[1]
|
||||
t.compare(insert.toString(), 'be')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
const testQuoteFormattedText = tc => {
|
||||
const doc = new Y.Doc()
|
||||
const text = /** @type {Y.XmlText} */ (doc.get('text', Y.XmlText))
|
||||
const text2 = /** @type {Y.XmlText} */ (doc.get('text2', Y.XmlText))
|
||||
|
||||
text.insert(0, 'abcde')
|
||||
text.format(1, 3, {i:true}) // 'a<i>bcd</i>e'
|
||||
const l1 = text.quote(0, 2) // 'a<i>b</i>'
|
||||
const l2 = text.quote(2, 1) // '<i>c</i>'
|
||||
const l3 = text.quote(3, 2) // '<i>d</i>e'
|
||||
|
||||
t.compare(l1.toString(), 'a<i>b</i>')
|
||||
t.compare(l2.toString(), '<i>c</i>')
|
||||
t.compare(l3.toString(), '<i>d</i>e')
|
||||
|
||||
text2.insertEmbed(0, l1)
|
||||
text2.insertEmbed(1, l2)
|
||||
text2.insertEmbed(2, l3)
|
||||
|
||||
const delta = text2.toDelta()
|
||||
t.compare(delta, [
|
||||
{insert: l1},
|
||||
{insert: l2},
|
||||
{insert: l3},
|
||||
])
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user