introduced Y.Range for inclusive and exclusive range definitions

This commit is contained in:
Bartosz Sypytkowski 2023-10-26 09:56:31 +09:00
parent f961aa960d
commit a01113812c
11 changed files with 441 additions and 157 deletions

View File

@ -17,6 +17,7 @@ export {
YArrayEvent, YArrayEvent,
YTextEvent, YTextEvent,
YEvent, YEvent,
YRange as Range,
Item, Item,
AbstractStruct, AbstractStruct,
GC, GC,

View File

@ -17,6 +17,7 @@ export * from './utils/Transaction.js'
export * from './utils/UndoManager.js' export * from './utils/UndoManager.js'
export * from './utils/updates.js' export * from './utils/updates.js'
export * from './utils/YEvent.js' export * from './utils/YEvent.js'
export * from './utils/YRange.js'
export * from './types/AbstractType.js' export * from './types/AbstractType.js'
export * from './types/YArray.js' export * from './types/YArray.js'

View File

@ -6,7 +6,7 @@ import {
readYXmlElement, readYXmlElement,
readYXmlFragment, readYXmlFragment,
readYXmlHook, readYXmlHook,
readYXmlText, readYXmlText,
readYWeakLink, readYWeakLink,
unlinkFrom, unlinkFrom,
YWeakLink, YWeakLink,

View File

@ -403,7 +403,7 @@ export class Item extends AbstractStruct {
if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
return this.parent.client return this.parent.client
} }
if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) { if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) {
// make sure that linked content is integrated first // make sure that linked content is integrated first
const content = /** @type {ContentType} */ (this.content) const content = /** @type {ContentType} */ (this.content)
@ -568,8 +568,8 @@ export class Item extends AbstractStruct {
// adjust length of parent // adjust length of parent
/** @type {AbstractType<any>} */ (this.parent)._length += this.length /** @type {AbstractType<any>} */ (this.parent)._length += this.length
} }
if (this.left && this.left.linked && this.right && this.right.linked) { if ((this.left && this.left.linked) || (this.right && this.right.linked)) {
// this item exists within a quoted range // this item may exists within a quoted range
joinLinkedRange(transaction, this) joinLinkedRange(transaction, this)
} }
} }

View File

@ -246,7 +246,7 @@ export const callTypeObservers = (type, transaction, event, visitedLinks = null)
} else if (type._item.linked) { } else if (type._item.linked) {
const linkedBy = transaction.doc.store.linkedBy.get(type._item) const linkedBy = transaction.doc.store.linkedBy.get(type._item)
if (linkedBy !== undefined) { if (linkedBy !== undefined) {
for (let link of linkedBy) { for (const link of linkedBy) {
if (visitedLinks === null || !visitedLinks.has(link)) { if (visitedLinks === null || !visitedLinks.has(link)) {
visitedLinks = visitedLinks !== null ? visitedLinks : new Set() visitedLinks = visitedLinks !== null ? visitedLinks : new Set()
visitedLinks.add(link) visitedLinks.add(link)

View File

@ -16,8 +16,8 @@ import {
YArrayRefID, YArrayRefID,
callTypeObservers, callTypeObservers,
transact, transact,
arrayWeakLink, quoteRange,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, YRange, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { typeListSlice } from './AbstractType.js' import { typeListSlice } from './AbstractType.js'
@ -201,24 +201,22 @@ export class YArray extends AbstractType {
get (index) { get (index) {
return typeListGet(this, index) return typeListGet(this, index)
} }
/** /**
* Returns the weak link that allows to refer and observe live changes of contents of an YArray. * Returns the weak link that allows to refer and observe live changes of contents of an YArray.
* It points at a consecutive range of elements, starting at give `index` and spanning over provided * It points at a consecutive range of elements, starting at give `index` and spanning over provided
* length of elements. * length of elements.
* *
* @param {number} index The index of the element to return from the YArray * @param {YRange} range quoted range
* @param {number} length The number of elements to include in returned weak link reference.
* @return {YWeakLink<T>} * @return {YWeakLink<T>}
*/ */
quote (index, length = 1) { quote (range) {
if (this.doc !== null) { if (this.doc !== null) {
return transact(this.doc, transaction => { 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')
} }
/** /**

View File

@ -233,7 +233,7 @@ export class YMap extends AbstractType {
get (key) { get (key) {
return /** @type {any} */ (typeMapGet(this, key)) return /** @type {any} */ (typeMapGet(this, key))
} }
/** /**
* Returns a weak reference link to another element stored in the same document. * Returns a weak reference link to another element stored in the same document.
* *

View File

@ -26,9 +26,9 @@ import {
typeMapGet, typeMapGet,
typeMapGetAll, typeMapGetAll,
updateMarkerChanges, updateMarkerChanges,
quoteRange,
ContentType, ContentType,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, // eslint-disable-line ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, YRange, // eslint-disable-line
quoteText
} from '../internals.js' } from '../internals.js'
import * as object from 'lib0/object' 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>` * `<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. * where `"<i>llo wo</i>"` represents quoted range.
* *
* @param {number} index The index where quoted range should start * @param {YRange} range
* @param {number} length Number of quoted elements
* @return {YWeakLink<string>} * @return {YWeakLink<string>}
* *
* @public * @public
*/ */
quote (index, length) { quote (range) {
const y = this.doc if (this.doc !== null) {
if (y !== null) { return transact(this.doc, transaction => {
return transact(y, transaction => { return quoteRange(transaction, this, range)
const pos = findPosition(transaction, this, index)
return quoteText(transaction, this, pos, length)
}) })
} }
throw new Error('Quoted text was not integrated into Doc')
throw new Error('cannot quote an YText that has not been integrated into YDoc')
} }
/** /**

View File

@ -1,11 +1,10 @@
import { decoding, encoding, error } from 'lib0' import { decoding, encoding } from 'lib0'
import * as map from 'lib0/map' import * as map from 'lib0/map'
import * as set from 'lib0/set' import * as set from 'lib0/set'
import { import {
YEvent, AbstractType, YEvent, AbstractType,
transact, transact,
getItemCleanEnd, getItemCleanEnd,
createID,
getItemCleanStart, getItemCleanStart,
callTypeObservers, callTypeObservers,
YWeakLinkRefID, YWeakLinkRefID,
@ -17,7 +16,7 @@ import {
formatXmlString, formatXmlString,
YText, YText,
YXmlText, 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' } from '../internals.js'
/** /**
@ -54,12 +53,13 @@ export class YWeakLink extends AbstractType {
this._quoteStart = start this._quoteStart = start
/** @type {RelativePosition} */ /** @type {RelativePosition} */
this._quoteEnd = end this._quoteEnd = end
/** @type {Item|null} */
this._firstItem = firstItem this._firstItem = firstItem
} }
/** /**
* Position descriptor of the start of a quoted range. * Position descriptor of the start of a quoted range.
* *
* @returns {RelativePosition} * @returns {RelativePosition}
*/ */
get quoteStart () { get quoteStart () {
@ -68,7 +68,7 @@ export class YWeakLink extends AbstractType {
/** /**
* Position descriptor of the end of a quoted range. * Position descriptor of the end of a quoted range.
* *
* @returns {RelativePosition} * @returns {RelativePosition}
*/ */
get quoteEnd () { get quoteEnd () {
@ -99,8 +99,8 @@ export class YWeakLink extends AbstractType {
// we don't support quotations over maps // we don't support quotations over maps
this._firstItem = item this._firstItem = item
} }
if (!this._firstItem.deleted) { if (!item.deleted) {
return this._firstItem.content.getContent()[0] return item.content.getContent()[0]
} }
} }
@ -114,18 +114,27 @@ export class YWeakLink extends AbstractType {
*/ */
unquote () { unquote () {
let result = /** @type {Array<any>} */ ([]) let result = /** @type {Array<any>} */ ([])
let item = this._firstItem let n = this._firstItem
if (n !== null && this._quoteStart.assoc >= 0) {
// if assoc >= we exclude start from range
n = n.right
}
const end = /** @type {ID} */ (this._quoteEnd.item) const end = /** @type {ID} */ (this._quoteEnd.item)
const endAssoc = this._quoteEnd.assoc
// TODO: moved elements // TODO: moved elements
while (item !== null) { while (n !== null) {
if (!item.deleted) { if (endAssoc < 0 && n.id.client === end.client && n.id.clock === end.clock) {
result = result.concat(item.content.getContent()) // right side is open (last item excluded)
}
const lastId = item.lastId
if (lastId.client === end.client && lastId.clock === end.clock) {
break 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 return result
} }
@ -147,8 +156,7 @@ export class YWeakLink extends AbstractType {
// link may refer to a single element in multi-element block // link may refer to a single element in multi-element block
// in such case we need to cut of the linked element into a // in such case we need to cut of the linked element into a
// separate block // separate block
let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item)) let [firstItem, lastItem] = sliceBlocksByRange(transaction, this._quoteStart, this.quoteEnd)
getItemCleanEnd(transaction, y.store, /** @type {ID} */(this._quoteEnd.item))
if (firstItem.parentSub !== null) { if (firstItem.parentSub !== null) {
// for maps, advance to most recent item // for maps, advance to most recent item
while (firstItem.right !== null) { while (firstItem.right !== null) {
@ -159,11 +167,9 @@ export class YWeakLink extends AbstractType {
/** @type {Item|null} */ /** @type {Item|null} */
let item = firstItem let item = firstItem
const end = /** @type {ID} */ (this._quoteEnd.item)
for (;item !== null; item = item.right) { for (;item !== null; item = item.right) {
createLink(transaction, item, this) createLink(transaction, item, this)
const lastId = item.lastId if (item === lastItem) {
if (lastId.client === end.client && lastId.clock === end.clock) {
break break
} }
} }
@ -216,22 +222,31 @@ export class YWeakLink extends AbstractType {
* @public * @public
*/ */
toString () { toString () {
if (this._firstItem !== null) { let n = this._firstItem
switch (/** @type {AbstractType<any>} */ (this._firstItem.parent).constructor) { 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: { case YText: {
let str = '' let str = ''
/**
* @type {Item|null}
*/
let n = this._firstItem
const end = /** @type {ID} */ (this._quoteEnd.item) const end = /** @type {ID} */ (this._quoteEnd.item)
const endAssoc = this._quoteEnd.assoc
while (n !== null) { 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) { if (!n.deleted && n.countable && n.content.constructor === ContentString) {
str += /** @type {ContentString} */ (n.content).str str += /** @type {ContentString} */ (n.content).str
} }
const lastId = n.lastId if (endAssoc >= 0) {
if (lastId.client === end.client && lastId.clock === end.clock) { const lastId = n.lastId
break if (lastId.client === end.client && lastId.clock === end.clock) {
// right side is closed (last item included)
break
}
} }
n = n.right n = n.right
} }
@ -278,107 +293,54 @@ export const readYWeakLink = decoder => {
return new YWeakLink(start, end, null) return new YWeakLink(start, end, null)
} }
const invalidQuotedRange = error.create('Invalid quoted range length.')
/** /**
* Returns a {WeakLink} to an YArray element at given index. * Returns a {WeakLink} to an YArray element at given index.
* *
* @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {Transaction} transaction
* @param {YRange} range
* @return {YWeakLink<any>} * @return {YWeakLink<any>}
*/ */
export const arrayWeakLink = (transaction, parent, index, length = 1) => { export const quoteRange = (transaction, parent, range) => {
if (length <= 0) { const [start, end] = rangeToRelative(parent, range)
throw invalidQuotedRange const [startItem, endItem] = sliceBlocksByRange(transaction, start, end)
} const link = new YWeakLink(start, end, startItem)
let startItem = parent._start if (parent.doc !== null) {
for (;startItem !== null; startItem = startItem.right) { transact(parent.doc, (transaction) => {
if (!startItem.deleted && startItem.countable) { for (let item = link._firstItem; item !== null; item = item = item.right) {
if (index < startItem.length) { createLink(transaction, item, link)
if (index > 0) { if (item === endItem) {
startItem = getItemCleanStart(transaction, createID(startItem.id.client, startItem.id.clock + index)) break
} }
break
} }
index -= startItem.length })
}
} }
let endItem = startItem return link
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)
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. * Checks relative position markers and slices the corresponding struct store items
* across their positions.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {RelativePosition} start
* @param {ItemTextListPosition} pos * @param {RelativePosition} end
* @param {number} length * @returns {Array<Item>} first and last item that belongs to a sliced range
* @return {YWeakLink<string>}
*/ */
export const quoteText = (transaction, parent, pos, length) => { const sliceBlocksByRange = (transaction, start, end) => {
if (pos.right !== null) { if (start.item === null || end.item === null) {
const startItem = pos.right throw new Error('this operation requires range to be bounded on both sides')
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
}
} }
const first = getItemCleanStart(transaction, start.item)
throw invalidQuotedRange /** @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)
}
return [first, last]
} }
/** /**
@ -459,6 +421,21 @@ export const joinLinkedRange = (transaction, item) => {
const common = new Set() const common = new Set()
for (const link of leftLinks) { for (const link of leftLinks) {
if (rightLinks.has(link)) { 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) common.add(link)
} }
} }
@ -466,4 +443,4 @@ export const joinLinkedRange = (transaction, item) => {
allLinks.set(item, common) allLinks.set(item, common)
} }
} }
} }

214
src/utils/YRange.js Normal file
View 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]
}

View File

@ -27,7 +27,7 @@ export const testBasicMap = tc => {
export const testBasicArray = tc => { export const testBasicArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 2 }) const { testConnector, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3]) 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(0), 1)
t.compare(array0.get(1), 2) t.compare(array0.get(1), 2)
@ -49,7 +49,7 @@ export const testArrayQuoteMultipleElements = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 2 }) const { testConnector, array0, array1 } = init(tc, { users: 2 })
const nested = new Y.Map([['key', 'value']]) const nested = new Y.Map([['key', 'value']])
array0.insert(0, [1, 2, nested, 3]) 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) const link0 = array0.get(0)
t.compare(link0.unquote(), [2, nested, 3]) t.compare(link0.unquote(), [2, nested, 3])
@ -89,7 +89,7 @@ export const testArrayQuoteMultipleElements = tc => {
export const testSelfQuotation = tc => { export const testSelfQuotation = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 2 }) const { testConnector, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3, 4]) 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 array0.insert(1, [link0]) // link is inserted into its own range
t.compare(link0.unquote(), [1, link0, 2, 3]) t.compare(link0.unquote(), [1, link0, 2, 3])
@ -259,7 +259,7 @@ export const testObserveMapDelete = tc => {
export const testObserveArray = tc => { export const testObserveArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 2 }) const { testConnector, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, ['A', 'B', 'C']) 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]) array0.insert(0, [link0])
/** /**
* @type {any} * @type {any}
@ -405,7 +405,7 @@ export const testDeepObserveMap = tc => {
const nested = new Y.Map() const nested = new Y.Map()
array.insert(0, [nested]) array.insert(0, [nested])
const link = array.quote(0) const link = array.quote(Y.Range.only(0))
map.set('link', link) map.set('link', link)
// update entry in linked map // update entry in linked map
@ -501,7 +501,7 @@ export const testDeepObserveNewElementWithinQuotedRange = tc => {
const m1 = new Y.Map() const m1 = new Y.Map()
const m3 = new Y.Map() const m3 = new Y.Map()
array0.insert(0, [1, m1, m3, 2]) 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]) array0.insert(0, [link0])
testConnector.flushAllMessages() testConnector.flushAllMessages()
@ -635,9 +635,9 @@ export const testDeepObserveRecursive = tc => {
root.insert(1, [m1]) root.insert(1, [m1])
root.insert(2, [m2]) root.insert(2, [m2])
const l0 = root.quote(0) const l0 = root.quote(Y.Range.only(0))
const l1 = root.quote(1) const l1 = root.quote(Y.Range.only(1))
const l2 = root.quote(2) const l2 = root.quote(Y.Range.only(2))
// create cyclic reference between links // create cyclic reference between links
m0.set('k1', l1) m0.set('k1', l1)
@ -649,7 +649,7 @@ export const testDeepObserveRecursive = tc => {
*/ */
let events = [] let events = []
m0.observeDeep((es) => { m0.observeDeep((es) => {
events = es.map((e) => { events = es.map((e) => {
return { target: e.target, keys: e.keys } return { target: e.target, keys: e.keys }
}) })
}) })
@ -709,7 +709,7 @@ export const testTextBasic = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 2 }) const { testConnector, text0, text1 } = init(tc, { users: 2 })
text0.insert(0, 'abcd') // 'abcd' 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') t.compare(link0.toString(), 'bc')
text0.insert(2, 'ef') // 'abefcd', quote: [befc] text0.insert(2, 'ef') // 'abefcd', quote: [befc]
t.compare(link0.toString(), 'befc') t.compare(link0.toString(), 'befc')
@ -733,7 +733,7 @@ export const testXmlTextBasic = tc => {
xml0.insert(0, [text0]) xml0.insert(0, [text0])
text0.insert(0, 'abcd') // 'abcd' 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') t.compare(link0.toString(), 'bc')
text0.insert(2, 'ef') // 'abefcd', quote: [befc] text0.insert(2, 'ef') // 'abefcd', quote: [befc]
t.compare(link0.toString(), 'befc') t.compare(link0.toString(), 'befc')
@ -747,6 +747,7 @@ export const testXmlTextBasic = tc => {
const { insert } = delta[1] // YWeakLink const { insert } = delta[1] // YWeakLink
t.compare(insert.toString(), 'be') t.compare(insert.toString(), 'be')
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@ -758,11 +759,11 @@ export const testQuoteFormattedText = tc => {
text.insert(0, 'abcde') text.insert(0, 'abcde')
text.format(0, 1, { b: true }) text.format(0, 1, { b: true })
text.format(1, 3, { i: true }) // '<b>a</b><i>bcd</i>e' 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>') 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>') 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') t.compare(l3.toString(), '<i>d</i>e')
text2.insertEmbed(0, l1) text2.insertEmbed(0, l1)
@ -775,4 +776,98 @@ export const testQuoteFormattedText = tc => {
{ insert: l2 }, { insert: l2 },
{ insert: l3 } { 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'])
}