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
 | 
			
		||||
    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 endAssoc = this._quoteEnd.assoc
 | 
			
		||||
    // TODO: moved elements
 | 
			
		||||
    while (item !== null) {
 | 
			
		||||
      if (!item.deleted) {
 | 
			
		||||
        result = result.concat(item.content.getContent())
 | 
			
		||||
      }
 | 
			
		||||
      const lastId = item.lastId
 | 
			
		||||
      if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
    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,22 +222,31 @@ 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
 | 
			
		||||
            }
 | 
			
		||||
            const lastId = n.lastId
 | 
			
		||||
            if (lastId.client === end.client && lastId.clock === end.clock) {
 | 
			
		||||
              break
 | 
			
		||||
            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
 | 
			
		||||
          }
 | 
			
		||||
@ -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))
 | 
			
		||||
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) => {
 | 
			
		||||
      for (let item = link._firstItem; item !== null; item = item = item.right) {
 | 
			
		||||
        createLink(transaction, item, link)
 | 
			
		||||
        if (item === endItem) {
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        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)
 | 
			
		||||
    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 link
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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()
 | 
			
		||||
    }
 | 
			
		||||
    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 sliceBlocksByRange = (transaction, start, end) => {
 | 
			
		||||
  if (start.item === null || end.item === null) {
 | 
			
		||||
    throw new Error('this operation requires range to be bounded on both sides')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throw invalidQuotedRange
 | 
			
		||||
  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)
 | 
			
		||||
  }
 | 
			
		||||
  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