introduced Y.Range for inclusive and exclusive range definitions
This commit is contained in:
parent
f961aa960d
commit
a01113812c
@ -17,6 +17,7 @@ export {
|
||||
YArrayEvent,
|
||||
YTextEvent,
|
||||
YEvent,
|
||||
YRange as Range,
|
||||
Item,
|
||||
AbstractStruct,
|
||||
GC,
|
||||
|
@ -17,6 +17,7 @@ export * from './utils/Transaction.js'
|
||||
export * from './utils/UndoManager.js'
|
||||
export * from './utils/updates.js'
|
||||
export * from './utils/YEvent.js'
|
||||
export * from './utils/YRange.js'
|
||||
|
||||
export * from './types/AbstractType.js'
|
||||
export * from './types/YArray.js'
|
||||
|
@ -568,8 +568,8 @@ export class Item extends AbstractStruct {
|
||||
// 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
|
||||
if ((this.left && this.left.linked) || (this.right && this.right.linked)) {
|
||||
// this item may exists within a quoted range
|
||||
joinLinkedRange(transaction, this)
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ export const callTypeObservers = (type, transaction, event, visitedLinks = null)
|
||||
} else if (type._item.linked) {
|
||||
const linkedBy = transaction.doc.store.linkedBy.get(type._item)
|
||||
if (linkedBy !== undefined) {
|
||||
for (let link of linkedBy) {
|
||||
for (const link of linkedBy) {
|
||||
if (visitedLinks === null || !visitedLinks.has(link)) {
|
||||
visitedLinks = visitedLinks !== null ? visitedLinks : new Set()
|
||||
visitedLinks.add(link)
|
||||
|
@ -16,8 +16,8 @@ import {
|
||||
YArrayRefID,
|
||||
callTypeObservers,
|
||||
transact,
|
||||
arrayWeakLink,
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line
|
||||
quoteRange,
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, YRange, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
import { typeListSlice } from './AbstractType.js'
|
||||
|
||||
@ -207,18 +207,16 @@ export class YArray extends AbstractType {
|
||||
* It points at a consecutive range of elements, starting at give `index` and spanning over provided
|
||||
* length of elements.
|
||||
*
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
* @param {number} length The number of elements to include in returned weak link reference.
|
||||
* @param {YRange} range quoted range
|
||||
* @return {YWeakLink<T>}
|
||||
*/
|
||||
quote (index, length = 1) {
|
||||
quote (range) {
|
||||
if (this.doc !== null) {
|
||||
return transact(this.doc, transaction => {
|
||||
return arrayWeakLink(transaction, this, index, length)
|
||||
return quoteRange(transaction, this, range)
|
||||
})
|
||||
} else {
|
||||
throw new Error('cannot quote an YArray that has not been integrated into YDoc')
|
||||
}
|
||||
throw new Error('cannot quote an YArray that has not been integrated into YDoc')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,9 +26,9 @@ import {
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
updateMarkerChanges,
|
||||
quoteRange,
|
||||
ContentType,
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, // eslint-disable-line
|
||||
quoteText
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, YRange, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as object from 'lib0/object'
|
||||
@ -1064,21 +1064,19 @@ export class YText extends AbstractType {
|
||||
* `<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
|
||||
* @param {YRange} range
|
||||
* @return {YWeakLink<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)
|
||||
quote (range) {
|
||||
if (this.doc !== null) {
|
||||
return transact(this.doc, transaction => {
|
||||
return quoteRange(transaction, this, range)
|
||||
})
|
||||
}
|
||||
throw new Error('Quoted text was not integrated into Doc')
|
||||
|
||||
throw new Error('cannot quote an YText that has not been integrated into YDoc')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { decoding, encoding, error } from 'lib0'
|
||||
import { decoding, encoding } from 'lib0'
|
||||
import * as map from 'lib0/map'
|
||||
import * as set from 'lib0/set'
|
||||
import {
|
||||
YEvent, AbstractType,
|
||||
transact,
|
||||
getItemCleanEnd,
|
||||
createID,
|
||||
getItemCleanStart,
|
||||
callTypeObservers,
|
||||
YWeakLinkRefID,
|
||||
@ -17,7 +16,7 @@ import {
|
||||
formatXmlString,
|
||||
YText,
|
||||
YXmlText,
|
||||
Transaction, Item, Doc, ID, Snapshot, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ItemTextListPosition // eslint-disable-line
|
||||
Transaction, Item, Doc, ID, Snapshot, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, YRange, rangeToRelative, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
@ -54,6 +53,7 @@ export class YWeakLink extends AbstractType {
|
||||
this._quoteStart = start
|
||||
/** @type {RelativePosition} */
|
||||
this._quoteEnd = end
|
||||
/** @type {Item|null} */
|
||||
this._firstItem = firstItem
|
||||
}
|
||||
|
||||
@ -99,8 +99,8 @@ export class YWeakLink extends AbstractType {
|
||||
// we don't support quotations over maps
|
||||
this._firstItem = item
|
||||
}
|
||||
if (!this._firstItem.deleted) {
|
||||
return this._firstItem.content.getContent()[0]
|
||||
if (!item.deleted) {
|
||||
return item.content.getContent()[0]
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,18 +114,27 @@ export class YWeakLink extends AbstractType {
|
||||
*/
|
||||
unquote () {
|
||||
let result = /** @type {Array<any>} */ ([])
|
||||
let item = this._firstItem
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
// TODO: moved elements
|
||||
while (item !== null) {
|
||||
if (!item.deleted) {
|
||||
result = result.concat(item.content.getContent())
|
||||
let n = this._firstItem
|
||||
if (n !== null && this._quoteStart.assoc >= 0) {
|
||||
// if assoc >= we exclude start from range
|
||||
n = n.right
|
||||
}
|
||||
const lastId = item.lastId
|
||||
if (lastId.client === end.client && lastId.clock === end.clock) {
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
const endAssoc = this._quoteEnd.assoc
|
||||
// TODO: moved elements
|
||||
while (n !== null) {
|
||||
if (endAssoc < 0 && n.id.client === end.client && n.id.clock === end.clock) {
|
||||
// right side is open (last item excluded)
|
||||
break
|
||||
}
|
||||
item = item.right
|
||||
if (!n.deleted) {
|
||||
result = result.concat(n.content.getContent())
|
||||
}
|
||||
const lastId = n.lastId
|
||||
if (endAssoc >= 0 && lastId.client === end.client && lastId.clock === end.clock) {
|
||||
break
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -147,8 +156,7 @@ export class YWeakLink extends AbstractType {
|
||||
// link may refer to a single element in multi-element block
|
||||
// in such case we need to cut of the linked element into a
|
||||
// separate block
|
||||
let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item))
|
||||
getItemCleanEnd(transaction, y.store, /** @type {ID} */(this._quoteEnd.item))
|
||||
let [firstItem, lastItem] = sliceBlocksByRange(transaction, this._quoteStart, this.quoteEnd)
|
||||
if (firstItem.parentSub !== null) {
|
||||
// for maps, advance to most recent item
|
||||
while (firstItem.right !== null) {
|
||||
@ -159,11 +167,9 @@ export class YWeakLink extends AbstractType {
|
||||
|
||||
/** @type {Item|null} */
|
||||
let item = firstItem
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
for (;item !== null; item = item.right) {
|
||||
createLink(transaction, item, this)
|
||||
const lastId = item.lastId
|
||||
if (lastId.client === end.client && lastId.clock === end.clock) {
|
||||
if (item === lastItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -216,23 +222,32 @@ export class YWeakLink extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
toString () {
|
||||
if (this._firstItem !== null) {
|
||||
switch (/** @type {AbstractType<any>} */ (this._firstItem.parent).constructor) {
|
||||
let n = this._firstItem
|
||||
if (n !== null && this._quoteStart.assoc >= 0) {
|
||||
// if assoc >= we exclude start from range
|
||||
n = n.right
|
||||
}
|
||||
if (n !== null) {
|
||||
switch (/** @type {AbstractType<any>} */ (n.parent).constructor) {
|
||||
case YText: {
|
||||
let str = ''
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._firstItem
|
||||
const end = /** @type {ID} */ (this._quoteEnd.item)
|
||||
const endAssoc = this._quoteEnd.assoc
|
||||
while (n !== null) {
|
||||
if (endAssoc < 0 && n.id.client === end.client && n.id.clock === end.clock) {
|
||||
// right side is open (last item excluded)
|
||||
break
|
||||
}
|
||||
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
}
|
||||
if (endAssoc >= 0) {
|
||||
const lastId = n.lastId
|
||||
if (lastId.client === end.client && lastId.clock === end.clock) {
|
||||
// right side is closed (last item included)
|
||||
break
|
||||
}
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return str
|
||||
@ -278,107 +293,54 @@ export const readYWeakLink = decoder => {
|
||||
return new YWeakLink(start, end, null)
|
||||
}
|
||||
|
||||
const invalidQuotedRange = error.create('Invalid quoted range length.')
|
||||
|
||||
/**
|
||||
* Returns a {WeakLink} to an YArray element at given index.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {Transaction} transaction
|
||||
* @param {YRange} range
|
||||
* @return {YWeakLink<any>}
|
||||
*/
|
||||
export const arrayWeakLink = (transaction, parent, index, length = 1) => {
|
||||
if (length <= 0) {
|
||||
throw invalidQuotedRange
|
||||
}
|
||||
let startItem = parent._start
|
||||
for (;startItem !== null; startItem = startItem.right) {
|
||||
if (!startItem.deleted && startItem.countable) {
|
||||
if (index < startItem.length) {
|
||||
if (index > 0) {
|
||||
startItem = getItemCleanStart(transaction, createID(startItem.id.client, startItem.id.clock + index))
|
||||
}
|
||||
break
|
||||
}
|
||||
index -= startItem.length
|
||||
}
|
||||
}
|
||||
let endItem = startItem
|
||||
let remaining = length
|
||||
for (;endItem !== null; endItem = endItem.right) {
|
||||
if (!endItem.deleted && endItem.countable) {
|
||||
if (remaining > endItem.length) {
|
||||
remaining -= endItem.length
|
||||
} else {
|
||||
endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + remaining - 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (startItem !== null && endItem !== null) {
|
||||
const start = new RelativePosition(null, null, startItem.id, 0)
|
||||
const end = new RelativePosition(null, null, endItem.lastId, -1)
|
||||
export const quoteRange = (transaction, parent, range) => {
|
||||
const [start, end] = rangeToRelative(parent, range)
|
||||
const [startItem, endItem] = sliceBlocksByRange(transaction, start, end)
|
||||
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) {
|
||||
if (item === endItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return link
|
||||
}
|
||||
throw invalidQuotedRange
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {WeakLink} to an YMap element at given key.
|
||||
* Checks relative position markers and slices the corresponding struct store items
|
||||
* across their positions.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {ItemTextListPosition} pos
|
||||
* @param {number} length
|
||||
* @return {YWeakLink<string>}
|
||||
* @param {RelativePosition} start
|
||||
* @param {RelativePosition} end
|
||||
* @returns {Array<Item>} first and last item that belongs to a sliced range
|
||||
*/
|
||||
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()
|
||||
const sliceBlocksByRange = (transaction, start, end) => {
|
||||
if (start.item === null || end.item === null) {
|
||||
throw new Error('this operation requires range to be bounded on both sides')
|
||||
}
|
||||
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 first = getItemCleanStart(transaction, start.item)
|
||||
/** @type {Item} */
|
||||
let last
|
||||
if (end.assoc >= 0) {
|
||||
last = getItemCleanEnd(transaction, transaction.doc.store, end.item)
|
||||
} else {
|
||||
const item = getItemCleanStart(transaction, end.item)
|
||||
last = /** @type {Item} */ (item.left)
|
||||
}
|
||||
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
|
||||
return [first, last]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -459,6 +421,21 @@ export const joinLinkedRange = (transaction, item) => {
|
||||
const common = new Set()
|
||||
for (const link of leftLinks) {
|
||||
if (rightLinks.has(link)) {
|
||||
// new item existing in a quoted range in between two elements
|
||||
common.add(link)
|
||||
} else if (link._quoteEnd.assoc < 0) {
|
||||
// We're at the right edge of quoted range - right neighbor is not included
|
||||
// but the left one is. Since quotation is open on the right side, we need to
|
||||
// include current item.
|
||||
common.add(link)
|
||||
}
|
||||
}
|
||||
for (const link of rightLinks) {
|
||||
if (!leftLinks.has(link) && link._firstItem === item.left && link._quoteStart.assoc >= 0) {
|
||||
// We're at the right edge of quoted range - right neighbor is not included
|
||||
// but the left one is. Since quotation is open on the right side, we need to
|
||||
// include current item.
|
||||
link._firstItem = item // this item is the new most left-wise
|
||||
common.add(link)
|
||||
}
|
||||
}
|
||||
|
214
src/utils/YRange.js
Normal file
214
src/utils/YRange.js
Normal file
@ -0,0 +1,214 @@
|
||||
import {
|
||||
createID,
|
||||
findMarker,
|
||||
createRelativePosition,
|
||||
AbstractType, RelativePosition, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* Object which describes bounded range of elements, together with inclusivity/exclusivity rules
|
||||
* operating over that range.
|
||||
*
|
||||
* These inclusivity rules bear extra meaning when it comes to concurrent inserts, that may
|
||||
* eventually happen ie. range `[1..2]` (both side inclusive) means that if a concurrent insert
|
||||
* would happen at the boundary between 2nd and 3rd index, it should **NOT** be a part of that
|
||||
* range, while range definition `[1..3)` (right side is open) while still describing similar
|
||||
* range in linear collection, would also span the range over the elements inserted concurrently
|
||||
* between 2nd and 3rd indexes.
|
||||
*/
|
||||
export class YRange {
|
||||
// API mirrored after: https://www.w3.org/TR/IndexedDB/#idbkeyrange
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number|null} lower a lower bound of a range (cannot be higher than upper)
|
||||
* @param {number|null} upper an upper bound of a range (cannot be less than lower)
|
||||
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
||||
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
||||
*/
|
||||
constructor (lower, upper, lowerOpen = false, upperOpen = false) {
|
||||
if (lower !== null && upper !== null && lower > upper) {
|
||||
throw new Error('Invalid range: lower bound is higher than upper bound')
|
||||
}
|
||||
/**
|
||||
* A lower bound of a range (cannot be higher than upper). Null if unbounded.
|
||||
* @type {number|null}
|
||||
*/
|
||||
this.lower = lower
|
||||
/**
|
||||
* An upper bound of a range (cannot be less than lower). Null if unbounded.
|
||||
* @type {number|null}
|
||||
*/
|
||||
this.upper = upper
|
||||
/**
|
||||
* If `true` lower is NOT included in the range.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.lowerOpen = lowerOpen
|
||||
/**
|
||||
* If `true` upper is NOT included in the range.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.upperOpen = upperOpen
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a range that only spans over a single element.
|
||||
*
|
||||
* @param {number} index
|
||||
* @returns {YRange}
|
||||
*/
|
||||
static only (index) {
|
||||
return new YRange(index, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a range instance, that's bounded on the lower side and
|
||||
* unbounded on the upper side.
|
||||
*
|
||||
* @param {number} lower a lower bound of a range
|
||||
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
||||
* @returns {YRange}
|
||||
*/
|
||||
static lowerBound (lower, lowerOpen = false) {
|
||||
return new YRange(lower, null, lowerOpen, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a range instance, that's unbounded on the lower side and
|
||||
* bounded on the upper side.
|
||||
*
|
||||
* @param {number} upper an upper bound of a range
|
||||
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
||||
* @returns {YRange}
|
||||
*/
|
||||
static upperBound (upper, upperOpen = false) {
|
||||
return new YRange(null, upper, false, upperOpen)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new range instance, bounded on both ends.
|
||||
*
|
||||
* @param {number} lower a lower bound of a range (cannot be higher than upper)
|
||||
* @param {number} upper an upper bound of a range (cannot be less than lower)
|
||||
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
||||
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
||||
*/
|
||||
static bound (lower, upper, lowerOpen = false, upperOpen = false) {
|
||||
return new YRange(lower, upper, lowerOpen, upperOpen)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a provided index is included in current range.
|
||||
*
|
||||
* @param {number} index
|
||||
* @returns {boolean}
|
||||
*/
|
||||
includes (index) {
|
||||
if (this.lower !== null && index < this.lower) {
|
||||
return false
|
||||
}
|
||||
if (this.upper !== null && index > this.upper) {
|
||||
return false
|
||||
}
|
||||
if (index === this.lower) {
|
||||
return !this.lowerOpen
|
||||
}
|
||||
if (index === this.upper) {
|
||||
return !this.upperOpen
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const indexOutOfBounds = new Error('index out of bounds')
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {number} index
|
||||
* @returns {{item: Item,index:number}|null}
|
||||
*/
|
||||
const findPosition = (type, index) => {
|
||||
if (type._searchMarker !== null) {
|
||||
const marker = findMarker(type, index)
|
||||
if (marker !== null) {
|
||||
return { item: marker.p, index: marker.index }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
let remaining = index
|
||||
let item = type._start
|
||||
for (; item !== null && remaining > 0; item = item.right) {
|
||||
if (!item.deleted && item.countable) {
|
||||
if (remaining < item.length) {
|
||||
break
|
||||
}
|
||||
remaining -= item.length
|
||||
}
|
||||
}
|
||||
if (item === null) {
|
||||
return null
|
||||
} else {
|
||||
return { item, index: index - remaining }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pair of values representing relative IDs of a range.
|
||||
*
|
||||
* @param {AbstractType<any>} type collection that range relates to
|
||||
* @param {YRange} range
|
||||
* @returns {RelativePosition[]}
|
||||
* @throws Will throw an error, if range indexes are out of an type's bounds.
|
||||
*/
|
||||
export const rangeToRelative = (type, range) => {
|
||||
/** @type {RelativePosition} */
|
||||
let start
|
||||
/** @type {RelativePosition} */
|
||||
let end
|
||||
let item = type._start
|
||||
let remaining = 0
|
||||
if (range.lower !== null) {
|
||||
remaining = range.lower
|
||||
if (remaining === 0 && item !== null) {
|
||||
start = createRelativePosition(type, item.id, range.lowerOpen ? 0 : -1)
|
||||
} else {
|
||||
const pos = findPosition(type, remaining)
|
||||
if (pos !== null) {
|
||||
item = pos.item
|
||||
remaining -= pos.index
|
||||
start = createRelativePosition(type, createID(pos.item.id.client, pos.item.id.clock + remaining), range.lowerOpen ? 0 : -1)
|
||||
} else {
|
||||
throw indexOutOfBounds
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// left-side unbounded
|
||||
start = createRelativePosition(type, null, -1)
|
||||
}
|
||||
|
||||
if (range.upper !== null) {
|
||||
remaining = range.upper - (range.lower ?? 0) + remaining
|
||||
while (item !== null) {
|
||||
if (!item.deleted && item.countable) {
|
||||
if (item.length > remaining) {
|
||||
break
|
||||
}
|
||||
remaining -= item.length
|
||||
}
|
||||
item = item.right
|
||||
}
|
||||
if (item === null) {
|
||||
throw indexOutOfBounds
|
||||
} else {
|
||||
end = createRelativePosition(type, createID(item.id.client, item.id.clock + remaining), range.upperOpen ? -1 : 0)
|
||||
}
|
||||
} else {
|
||||
// right-side unbounded
|
||||
end = createRelativePosition(type, null, 0)
|
||||
}
|
||||
return [start, end]
|
||||
}
|
@ -27,7 +27,7 @@ export const testBasicMap = tc => {
|
||||
export const testBasicArray = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 2 })
|
||||
array0.insert(0, [1, 2, 3])
|
||||
array0.insert(3, [array0.quote(1)])
|
||||
array0.insert(3, [array0.quote(Y.Range.only(1))])
|
||||
|
||||
t.compare(array0.get(0), 1)
|
||||
t.compare(array0.get(1), 2)
|
||||
@ -49,7 +49,7 @@ export const testArrayQuoteMultipleElements = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 2 })
|
||||
const nested = new Y.Map([['key', 'value']])
|
||||
array0.insert(0, [1, 2, nested, 3])
|
||||
array0.insert(0, [array0.quote(1, 3)])
|
||||
array0.insert(0, [array0.quote(Y.Range.bound(1, 3))])
|
||||
|
||||
const link0 = array0.get(0)
|
||||
t.compare(link0.unquote(), [2, nested, 3])
|
||||
@ -89,7 +89,7 @@ export const testArrayQuoteMultipleElements = tc => {
|
||||
export const testSelfQuotation = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 2 })
|
||||
array0.insert(0, [1, 2, 3, 4])
|
||||
const link0 = array0.quote(0, 3)
|
||||
const link0 = array0.quote(Y.Range.bound(0, 3, false, true))
|
||||
array0.insert(1, [link0]) // link is inserted into its own range
|
||||
|
||||
t.compare(link0.unquote(), [1, link0, 2, 3])
|
||||
@ -259,7 +259,7 @@ export const testObserveMapDelete = tc => {
|
||||
export const testObserveArray = tc => {
|
||||
const { testConnector, array0, array1 } = init(tc, { users: 2 })
|
||||
array0.insert(0, ['A', 'B', 'C'])
|
||||
const link0 = /** @type {Y.WeakLink<String>} */ (array0.quote(1, 2))
|
||||
const link0 = /** @type {Y.WeakLink<String>} */ (array0.quote(Y.Range.bound(1, 2)))
|
||||
array0.insert(0, [link0])
|
||||
/**
|
||||
* @type {any}
|
||||
@ -405,7 +405,7 @@ export const testDeepObserveMap = tc => {
|
||||
|
||||
const nested = new Y.Map()
|
||||
array.insert(0, [nested])
|
||||
const link = array.quote(0)
|
||||
const link = array.quote(Y.Range.only(0))
|
||||
map.set('link', link)
|
||||
|
||||
// update entry in linked map
|
||||
@ -501,7 +501,7 @@ export const testDeepObserveNewElementWithinQuotedRange = tc => {
|
||||
const m1 = new Y.Map()
|
||||
const m3 = new Y.Map()
|
||||
array0.insert(0, [1, m1, m3, 2])
|
||||
const link0 = array0.quote(1, 2)
|
||||
const link0 = array0.quote(Y.Range.bound(1, 2))
|
||||
array0.insert(0, [link0])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
@ -635,9 +635,9 @@ export const testDeepObserveRecursive = tc => {
|
||||
root.insert(1, [m1])
|
||||
root.insert(2, [m2])
|
||||
|
||||
const l0 = root.quote(0)
|
||||
const l1 = root.quote(1)
|
||||
const l2 = root.quote(2)
|
||||
const l0 = root.quote(Y.Range.only(0))
|
||||
const l1 = root.quote(Y.Range.only(1))
|
||||
const l2 = root.quote(Y.Range.only(2))
|
||||
|
||||
// create cyclic reference between links
|
||||
m0.set('k1', l1)
|
||||
@ -709,7 +709,7 @@ export const testTextBasic = tc => {
|
||||
const { testConnector, text0, text1 } = init(tc, { users: 2 })
|
||||
|
||||
text0.insert(0, 'abcd') // 'abcd'
|
||||
const link0 = text0.quote(1, 2) // quote: [bc]
|
||||
const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc]
|
||||
t.compare(link0.toString(), 'bc')
|
||||
text0.insert(2, 'ef') // 'abefcd', quote: [befc]
|
||||
t.compare(link0.toString(), 'befc')
|
||||
@ -733,7 +733,7 @@ export const testXmlTextBasic = tc => {
|
||||
xml0.insert(0, [text0])
|
||||
|
||||
text0.insert(0, 'abcd') // 'abcd'
|
||||
const link0 = text0.quote(1, 2) // quote: [bc]
|
||||
const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc]
|
||||
t.compare(link0.toString(), 'bc')
|
||||
text0.insert(2, 'ef') // 'abefcd', quote: [befc]
|
||||
t.compare(link0.toString(), 'befc')
|
||||
@ -747,6 +747,7 @@ export const testXmlTextBasic = tc => {
|
||||
const { insert } = delta[1] // YWeakLink
|
||||
t.compare(insert.toString(), 'be')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
@ -758,11 +759,11 @@ export const testQuoteFormattedText = tc => {
|
||||
text.insert(0, 'abcde')
|
||||
text.format(0, 1, { b: true })
|
||||
text.format(1, 3, { i: true }) // '<b>a</b><i>bcd</i>e'
|
||||
const l1 = text.quote(0, 2)
|
||||
const l1 = text.quote(Y.Range.bound(0, 1))
|
||||
t.compare(l1.toString(), '<b>a</b><i>b</i>')
|
||||
const l2 = text.quote(2, 1) // '<i>c</i>'
|
||||
const l2 = text.quote(Y.Range.only(2)) // '<i>c</i>'
|
||||
t.compare(l2.toString(), '<i>c</i>')
|
||||
const l3 = text.quote(3, 2) // '<i>d</i>e'
|
||||
const l3 = text.quote(Y.Range.bound(3, 4)) // '<i>d</i>e'
|
||||
t.compare(l3.toString(), '<i>d</i>e')
|
||||
|
||||
text2.insertEmbed(0, l1)
|
||||
@ -776,3 +777,97 @@ export const testQuoteFormattedText = tc => {
|
||||
{ insert: l3 }
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testTextLowerBoundary = tc => {
|
||||
const { testConnector, text0, text1, array0 } = init(tc, { users: 2 })
|
||||
text0.insert(0, 'abcdef')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4]
|
||||
const linkExclusive = text0.quote(Y.Range.bound(0, 4, true, false)) // (0..4]
|
||||
array0.insert(0, [linkInclusive, linkExclusive])
|
||||
t.compare(linkInclusive.toString(), 'bcde')
|
||||
t.compare(linkExclusive.toString(), 'bcde')
|
||||
|
||||
text1.insert(1, 'xyz')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.compare(linkInclusive.toString(), 'bcde')
|
||||
t.compare(linkExclusive.toString(), 'xyzbcde')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testTextUpperBoundary = tc => {
|
||||
const { testConnector, text0, text1, array0 } = init(tc, { users: 2 })
|
||||
text0.insert(0, 'abcdef')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4]
|
||||
const linkExclusive = text0.quote(Y.Range.bound(1, 5, false, true)) // [1..5)
|
||||
array0.insert(0, [linkInclusive, linkExclusive])
|
||||
t.compare(linkInclusive.toString(), 'bcde')
|
||||
t.compare(linkExclusive.toString(), 'bcde')
|
||||
|
||||
text1.insert(5, 'xyz')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.compare(linkInclusive.toString(), 'bcde')
|
||||
t.compare(linkExclusive.toString(), 'bcdexyz')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testArrayLowerBoundary = tc => {
|
||||
const { testConnector, array0, array1, map0 } = init(tc, { users: 2 })
|
||||
array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f'])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4]
|
||||
const linkExclusive = array0.quote(Y.Range.bound(0, 4, true, false)) // (0..4]
|
||||
map0.set('inclusive', linkInclusive)
|
||||
map0.set('exclusive', linkExclusive)
|
||||
t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
|
||||
array1.insert(1, ['x', 'y', 'z'])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
t.compare(linkExclusive.unquote(), ['x', 'y', 'z', 'b', 'c', 'd', 'e'])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testArrayUpperBoundary = tc => {
|
||||
const { testConnector, array0, array1, map0 } = init(tc, { users: 2 })
|
||||
array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f'])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4]
|
||||
const linkExclusive = array0.quote(Y.Range.bound(1, 5, false, true)) // [1..5)
|
||||
map0.set('inclusive', linkInclusive)
|
||||
map0.set('exclusive', linkExclusive)
|
||||
t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
|
||||
array1.insert(5, ['x', 'y', 'z'])
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e'])
|
||||
t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e', 'x', 'y', 'z'])
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user