pax_global_header00006660000000000000000000000064145020043040014502gustar00rootroot0000000000000052 comment=2fe8907ab0a2e07f5e735e895efbb15dbb751283 yjs-13.6.8/000077500000000000000000000000001450200430400124065ustar00rootroot00000000000000yjs-13.6.8/.github/000077500000000000000000000000001450200430400137465ustar00rootroot00000000000000yjs-13.6.8/.github/workflows/000077500000000000000000000000001450200430400160035ustar00rootroot00000000000000yjs-13.6.8/.github/workflows/node.js.yml000066400000000000000000000013561450200430400200730ustar00rootroot00000000000000# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint - run: npm run test-extensive env: CI: true yjs-13.6.8/.gitignore000066400000000000000000000000371450200430400143760ustar00rootroot00000000000000node_modules dist .vscode docs yjs-13.6.8/.jsdoc.json000066400000000000000000000020441450200430400144610ustar00rootroot00000000000000{ "sourceType": "module", "tags": { "allowUnknownTags": true, "dictionaries": ["jsdoc"] }, "source": { "include": ["./src"], "includePattern": ".js$" }, "plugins": [ "plugins/markdown" ], "templates": { "referenceTitle": "Yjs", "disableSort": false, "useCollapsibles": true, "collapse": true, "resources": { "yjs.dev": "Website", "docs.yjs.dev": "Docs", "discuss.yjs.dev": "Forum", "https://gitter.im/Yjs/community": "Chat" }, "logo": { "url": "https://yjs.dev/images/logo/yjs-512x512.png", "width": "162px", "height": "162px", "link": "/" }, "tabNames": { "api": "API", "tutorials": "Examples" }, "footerText": "Shared Editing", "css": [ "./style.css" ], "default": { "staticFiles": { "include": [] } } }, "opts": { "destination": "./docs/", "encoding": "utf8", "private": false, "recurse": true, "template": "./node_modules/tui-jsdoc-template" } } yjs-13.6.8/.markdownlint.json000066400000000000000000000000611450200430400160650ustar00rootroot00000000000000{ "default": true, "no-inline-html": false } yjs-13.6.8/INTERNALS.md000066400000000000000000000217211450200430400142720ustar00rootroot00000000000000# Yjs Internals This document roughly explains how Yjs works internally. There is a complete walkthrough of the Yjs codebase available as a recording: https://youtu.be/0l5XgnQ6rB4 The Yjs CRDT algorithm is described in the [YATA paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types) from 2016. For an algorithmic view of how it works, the paper is a reasonable place to start. There are a handful of small improvements implemented in Yjs which aren't described in the paper. The most notable is that items have an `originRight` as well as an `origin` property, which improves performance when many concurrent inserts happen after the same character. At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to reuse the CRDT resolution algorithm: - Arrays are easy - they're lists of arbitrary items. - Text is a list of characters, optionally punctuated by formatting markers and embeds for rich text support. Several characters can be wrapped in a single linked list `Item` (this is also known as the compound representation of CRDTs). More information about this in [this blog article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/). - Maps are lists of entries. The last inserted entry for each key is used, and all other duplicates for each key are flagged as deleted. Each client is assigned a unique *clientID* property on first insert. This is a random 53-bit integer (53 bits because that fits in the javascript safe integer range). ## List items Each item in a Yjs list is made up of two objects: - An `Item` (*src/structs/Item.js*). This is used to relate the item to other adjacent items. - An object in the `AbstractType` hierarchy (subclasses of *src/types/AbstractType.js* - eg `YText`). This stores the actual content in the Yjs document. The item and type object pair have a 1-1 mapping. The item's `content` field references the AbstractType object and the AbstractType object's `_item` field references the item. Everything inserted in a Yjs document is given a unique ID, formed from a *ID(clientID, clock)* pair (also known as a [Lamport Timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp)). The clock counts up from 0 with the first inserted character or item a client makes. This is similar to automerge's operation IDs, but note that the clock is only incremented by inserts. Deletes are handled in a very different way (see below). If a run of characters is inserted into a document (eg `"abc"`), the clock will be incremented for each character (eg 3 times here). But Yjs will only add a single `Item` into the list. This has no effect on the core CRDT algorithm, but the optimization dramatically decreases the number of javascript objects created during normal text editing. This optimization only applies if the characters share the same clientID, they're inserted in order, and all characters have either been deleted or all characters are not deleted. The item will be split if the run is interrupted for any reason (eg a character in the middle of the run is deleted). When an item is created, it stores a reference to the IDs of the preceeding and succeeding item. These are stored in the item's `origin` and `originRight` fields, respectively. These are used when peers concurrently insert at the same location in a document. Though quite rare in practice, Yjs needs to make sure the list items always resolve to the same order on all peers. The actual logic is relatively simple - its only a couple dozen lines of code and it lives in the `Item#integrate()` method. The YATA paper has much more detail on this algorithm. ### Item Storage The items themselves are stored in two data structures and a cache: - The items are stored in a tree of doubly-linked lists in *document order*. Each item has `left` and `right` properties linking to its siblings in the document. Items also have a `parent` property to reference their parent in the document tree (null at the root). (And you can access an item's children, if any, through `item.content`). - All items are referenced in *insertion order* inside the struct store (*src/utils/StructStore.js*). This references the list of items inserted by for each client, in chronological order. This is used to find an item in the tree with a given ID (using a binary search). It is also used to efficiently gather the operations a peer is missing during sync (more on this below). When a local insert happens, Yjs needs to map the insert position in the document (eg position 1000) to an ID. With just the linked list, this would require a slow O(n) linear scan of the list. But when editing a document, most inserts are either at the same position as the last insert, or nearby. To improve performance, Yjs stores a cache of the 10 most recently looked up insert positions in the document. This is consulted and updated when a position is looked up to improve performance in the average case. The cache is updated using a heuristic that is still changing (currently, it is updated when a new position significantly diverges from existing markers in the cache). Internally this is referred to as the skip list / fast search marker. ### Deletions Deletions in Yjs are treated very differently from insertions. Insertions are implemented as a sequential operation based CRDT, but deletions are treated as a simpler state based CRDT. When an item has been deleted by any peer, at any point in history, it is flagged as deleted on the item. (Internally Yjs uses the `info` bitfield.) Yjs does not record metadata about a deletion: - No data is kept on *when* an item was deleted, or which user deleted it. - The struct store does not contain deletion records - The clientID's clock is not incremented If garbage collection is enabled in Yjs, when an object is deleted its content is discarded. If a deleted object contains children (eg a field is deleted in an object), the content is replaced with a `GC` object (*src/structs/GC.js*). This is a very lightweight structure - it only stores the length of the removed content. Yjs has some special logic to share which content in a document has been deleted: - When a delete happens, as well as marking the item, the deleted IDs are listed locally within the transaction. (See below for more information about transactions.) When a transaction has been committed locally, the set of deleted items is appended to a transaction's update message. - A snapshot (a marked point in time in the Yjs history) is specified using both the set of (clientID, clock) pairs *and* the set of all deleted item IDs. The deleted set is O(n), but because deletions usually happen in runs, this data set is usually tiny in practice. (The real world editing trace from the B4 benchmark document contains 182k inserts and 77k deleted characters. The deleted set size in a snapshot is only 4.5Kb). ## Transactions All updates in Yjs happen within a *transaction*. (Defined in *src/utils/Transaction.js*.) The transaction collects a set of updates to the Yjs document to be applied on remote peers atomically. Once a transaction has been committed locally, it generates a compressed *update message* which is broadcast to synchronized remote peers to notify them of the local change. The update message contains: - The set of newly inserted items - The set of items deleted within the transaction. ## Network protocol The network protocol is not really a part of Yjs. There are a few relevant concepts that can be used to create a custom network protocol: * `update`: The Yjs document can be encoded to an *update* object that can be parsed to reconstruct the document. Also every change on the document fires an incremental document updates that allows clients to sync with each other. The update object is an Uint8Array that efficiently encodes `Item` objects and the delete set. * `state vector`: A state vector defines the known state of each user (a set of tuples `(client, clock)`). This object is also efficiently encoded as a Uint8Array. The client can ask a remote client for missing document updates by sending their state vector (often referred to as *sync step 1*). The remote peer can compute the missing `Item` objects using the `clocks` of the respective clients and compute a minimal update message that reflects all missing updates (sync step 2). An implementation of the syncing process is in [y-protocols](https://github.com/yjs/y-protocols). ## Snapshots A snapshot can be used to restore an old document state. It is a `state vector` \+ `delete set`. A client can restore an old document state by iterating through the sequence CRDT and ignoring all Items that have an `id.clock > stateVector[id.client].clock`. Instead of using `item.deleted` the client will use the delete set to find out if an item was deleted or not. It is not recommended to restore an old document state using snapshots, although that would certainly be possible. Instead, the old state should be computed by iterating through the newest state and using the additional information from the state vector. yjs-13.6.8/LICENSE000066400000000000000000000022731450200430400134170ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 - Kevin Jahns . - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. yjs-13.6.8/README.md000066400000000000000000001376661450200430400137100ustar00rootroot00000000000000 # ![Yjs](https://yjs.dev/images/logo/yjs-120x120.png) > A CRDT framework with a powerful abstraction of shared data Yjs is a [CRDT implementation](#Yjs-CRDT-Algorithm) that exposes its internal data structure as *shared types*. Shared types are common data types like `Map` or `Array` with superpowers: changes are automatically distributed to other peers and merged without merge conflicts. Yjs is **network agnostic** (p2p!), supports many existing **rich text editors**, **offline editing**, **version snapshots**, **undo/redo** and **shared cursors**. It scales well with an unlimited number of users and is well suited for even large documents. * Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos) * Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev) * Chat: [Gitter](https://gitter.im/Yjs/community) | [Discord](https://discord.gg/T3nqMT6qbM) * Benchmark Yjs vs. Automerge: [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks) * Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0) * Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/) :construction_worker_woman: If you are looking for professional support, please consider supporting this project via a "support contract" on [GitHub Sponsors](https://github.com/sponsors/dmonad). I will attend your issues quicker and we can discuss questions and problems in regular video conferences. Otherwise you can find help on our community [discussion board](https://discuss.yjs.dev). ## Sponsorship Please contribute to the project financially - especially if your company relies on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad) ## Who is using Yjs * [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source knowledge base. πŸ… * [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2: * [Sana](https://sanalabs.com/) A learning platform with collaborative text editing powered by Yjs. * [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and community. :star: * [Room.sh](https://room.sh/) A meeting application with integrated collaborative drawing, editing, and coding tools. :star: * [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by Nimbus Web. :star: * [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to collaboratively organize radio broadcasts. :star: * [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted collaborative notes app. * [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)* * [Alldone](https://alldone.app/) A next-gen project management and collaboration platform. * [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate. * [Slidebeamer](https://slidebeamer.com/) Presentation app. * [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys. * [Skiff](https://skiff.org/) Private, decentralized workspace. * [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for sharing analyses, documentation, spreadsheets, and dashboards. * [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon footprint calculator has a group P2P mode based on yjs * [oorja.io](https://oorja.io) Online meeting spaces extensible with collaborative apps, end-to-end encrypted. * [LegendKeeper](https://legendkeeper.com) Collaborative campaign planner and worldbuilding app for tabletop RPGs. * [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I. ## Table of Contents * [Overview](#Overview) * [Bindings](#Bindings) * [Providers](#Providers) * [Ports](#Ports) * [Getting Started](#Getting-Started) * [API](#API) * [Shared Types](#Shared-Types) * [Y.Doc](#YDoc) * [Document Updates](#Document-Updates) * [Relative Positions](#Relative-Positions) * [Y.UndoManager](#YUndoManager) * [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm) * [License and Author](#License-and-Author) ## Overview This repository contains a collection of shared types that can be observed for changes and manipulated concurrently. Network functionality and two-way-bindings are implemented in separate modules. ### Bindings | Name | Cursors | Binding | Demo | |---|:-:|---|---| | [ProseMirror](https://prosemirror.net/)                                                   | βœ” | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) | | [Quill](https://quilljs.com/) | βœ” | [y-quill](https://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) | | [CodeMirror](https://codemirror.net/) | βœ” | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) | | [Monaco](https://microsoft.github.io/monaco-editor/) | βœ” | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) | | [Slate](https://github.com/ianstormtaylor/slate) | βœ” | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) | | [BlockSuite](https://github.com/toeverything/blocksuite) | βœ” | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) | | [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) | | [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) | | React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) | ### Providers Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. **Providers** manage all that for you and are the perfect starting point for your collaborative app.
y-webrtc
Propagates document updates peer-to-peer using WebRTC. The peers exchange signaling data over signaling servers. Publically available signaling servers are available. Communication over the signaling servers can be encrypted by providing a shared secret, keeping the connection information and the shared document private.
y-websocket
A module that contains a simple websocket backend and a websocket client that connects to that backend. The backend can be extended to persist updates in a leveldb database.
y-indexeddb
Efficiently persists document updates to the browsers indexeddb database. The document is immediately available and only diffs need to be synced through the network provider.
y-libp2p
Uses libp2p to propagate updates via GossipSub. Also includes a peer-sync mechanism to catch up on missed updates.
y-dat
[WIP] Write document updates efficiently to the dat network using multifeed. Each client has an append-only log of CRDT local updates (hypercore). Multifeed manages and sync hypercores and y-dat listens to changes and applies them to the Yjs document.
@liveblocks/yjs
Liveblocks Yjs provides a fully hosted WebSocket infrastructure and persisted data store for Yjs documents. No configuration or maintenance is required. It also features Yjs webhook events, REST API to read and update Yjs documents, and a browser DevTools extension.
Matrix-CRDT
Use Matrix as an off-the-shelf backend for Yjs by using the MatrixProvider. Use Matrix as transport and storage of Yjs updates, so you can focus building your client app and Matrix can provide powerful features like Authentication, Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End Encryption (E2EE).
y-mongodb-provider
Adds persistent storage to a server with MongoDB. Can be used with the y-websocket provider.
@toeverything/y-indexeddb
Like y-indexeddb, but with sub-documents support and fully TypeScript.
# Ports There are several Yjs-compatible ports to other programming languages. * [y-octo](https://github.com/toeverything/y-octo) - Rust implementation by [AFFiNE](https://affine.pro) * [y-crdt](https://github.com/y-crdt/y-crdt) - Rust implementation with multiple language bindings to other languages * [yrs](https://github.com/y-crdt/y-crdt/tree/main/yrs) - Rust interface * [ypy](https://github.com/y-crdt/ypy) - Python binding * [yrb](https://github.com/y-crdt/yrb) - Ruby binding * [yrb](https://github.com/y-crdt/yswift) - Swift binding * [yffi](https://github.com/y-crdt/y-crdt/tree/main/yffi) - C-FFI * [ywasm](https://github.com/y-crdt/y-crdt/tree/main/ywasm) - WASM binding * [ycs](https://github.com/yjs/ycs) - .Net compatible C# implementation. ## Getting Started Install Yjs and a provider with your favorite package manager: ```sh npm i yjs y-websocket ``` Start the y-websocket server: ```sh PORT=1234 node ./node_modules/y-websocket/bin/server.js ``` ### Example: Observe types ```js import * as Y from 'yjs'; const doc = new Y.Doc(); const yarray = doc.getArray('my-array') yarray.observe(event => { console.log('yarray was modified') }) // every time a local or remote client modifies yarray, the observer is called yarray.insert(0, ['val']) // => "yarray was modified" ``` ### Example: Nest types Remember, shared types are just plain old data types. The only limitation is that a shared type must exist only once in the shared document. ```js const ymap = doc.getMap('map') const foodArray = new Y.Array() foodArray.insert(0, ['apple', 'banana']) ymap.set('food', foodArray) ymap.get('food') === foodArray // => true ymap.set('fruit', foodArray) // => Error! foodArray is already defined ``` Now you understand how types are defined on a shared document. Next you can jump to the [demo repository](https://github.com/yjs/yjs-demos) or continue reading the API docs. ### Example: Using and combining providers Any of the Yjs providers can be combined with each other. So you can sync data over different network technologies. In most cases you want to use a network provider (like y-websocket or y-webrtc) in combination with a persistence provider (y-indexeddb in the browser). Persistence allows you to load the document faster and to persist data that is created while offline. For the sake of this demo we combine two different network providers with a persistence provider. ```js import * as Y from 'yjs' import { WebrtcProvider } from 'y-webrtc' import { WebsocketProvider } from 'y-websocket' import { IndexeddbPersistence } from 'y-indexeddb' const ydoc = new Y.Doc() // this allows you to instantly get the (cached) documents data const indexeddbProvider = new IndexeddbPersistence('count-demo', ydoc) indexeddbProvider.whenSynced.then(() => { console.log('loaded data from indexed db') }) // Sync clients with the y-webrtc provider. const webrtcProvider = new WebrtcProvider('count-demo', ydoc) // Sync clients with the y-websocket provider const websocketProvider = new WebsocketProvider( 'wss://demos.yjs.dev', 'count-demo', ydoc ) // array of numbers which produce a sum const yarray = ydoc.getArray('count') // observe changes of the sum yarray.observe(event => { // print updates when the data changes console.log('new sum: ' + yarray.toArray().reduce((a,b) => a + b)) }) // add 1 to the sum yarray.push([1]) // => "new sum: 1" ``` ## API ```js import * as Y from 'yjs' ``` ### Shared Types
Y.Array

A shareable Array-like type that supports efficient insert/delete of elements at any position. Internally it uses a linked list of Arrays that is split when necessary.

const yarray = new Y.Array()
parent:Y.AbstractType|null
insert(index:number, content:Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
Insert content at index. Note that content is an array of elements. I.e. array.insert(0, [1]) splices the list and inserts 1 at position 0.
push(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
unshift(Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>)
delete(index:number, length:number)
get(index:number)
slice(start:number, end:number):Array<Object|boolean|Array|string|number|null|Uint8Array|Y.Type>
Retrieve a range of content
length:number
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type, index:number, array: Y.Array))
map(function(T, number, YArray):M):Array<M>
toArray():Array<object|boolean|Array|string|number|null|Uint8Array|Y.Type>
Copies the content of this YArray to a new Array.
toJSON():Array<Object|boolean|Array|string|number|null>
Copies the content of this YArray to a new Array. It transforms all child types to JSON using their toJSON method.
[Symbol.Iterator]
Returns an YArray Iterator that contains the values for each index in the array.
for (let value of yarray) { .. }
observe(function(YArrayEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
unobserve(function(YArrayEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.Map

A shareable Map type.

const ymap = new Y.Map()
parent:Y.AbstractType|null
size: number
Total number of key/value pairs.
get(key:string):object|boolean|string|number|null|Uint8Array|Y.Type
set(key:string, value:object|boolean|string|number|null|Uint8Array|Y.Type)
delete(key:string)
has(key:string):boolean
get(index:number)
clear()
Removes all elements from this YMap.
clone():Y.Map
Clone this type into a fresh Yjs type.
toJSON():Object<string, Object|boolean|Array|string|number|null|Uint8Array>
Copies the [key,value] pairs of this YMap to a new Object.It transforms all child types to JSON using their toJSON method.
forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type, key:string, map: Y.Map))
Execute the provided function once for every key-value pair.
[Symbol.Iterator]
Returns an Iterator of [key, value] pairs.
for (let [key, value] of ymap) { .. }
entries()
Returns an Iterator of [key, value] pairs.
values()
Returns an Iterator of all values.
keys()
Returns an Iterator of all keys.
observe(function(YMapEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
unobserve(function(YMapEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.Text

A shareable type that is optimized for shared editing on text. It allows to assign properties to ranges in the text. This makes it possible to implement rich-text bindings to this type.

This type can also be transformed to the delta format. Similarly the YTextEvents compute changes as deltas.

const ytext = new Y.Text()
parent:Y.AbstractType|null
insert(index:number, content:string, [formattingAttributes:Object<string,string>])
Insert a string at index and assign formatting attributes to it.
ytext.insert(0, 'bold text', { bold: true })
delete(index:number, length:number)
format(index:number, length:number, formattingAttributes:Object<string,string>)
Assign formatting attributes to a range in the text
applyDelta(delta: Delta, opts:Object<string,any>)
See Quill Delta Can set options for preventing remove ending newLines, default is true.
ytext.applyDelta(delta, { sanitize: false })
length:number
toString():string
Transforms this type, without formatting options, into a string.
toJSON():string
See toString
toDelta():Delta
Transforms this type to a Quill Delta
observe(function(YTextEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
unobserve(function(YTextEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.XmlFragment

A container that holds an Array of Y.XmlElements.

const yxml = new Y.XmlFragment()
parent:Y.AbstractType|null
firstChild:Y.XmlElement|Y.XmlText|null
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number)
get(index:number)
slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText>
Retrieve a range of content
length:number
clone():Y.XmlFragment
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
toDOM():DocumentFragment
Transforms this type and all children to new DOM elements.
toString():string
Get the XML serialization of all descendants.
toJSON():string
See toString.
createTreeWalker(filter: function(AbstractType<any>):boolean):Iterable
Create an Iterable that walks through the children.
observe(function(YXmlEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
unobserve(function(YXmlEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
Y.XmlElement

A shareable type that represents an XML Element. It has a nodeName, attributes, and a list of children. But it makes no effort to validate its content and be actually XML compliant.

const yxml = new Y.XmlElement()
parent:Y.AbstractType|null
firstChild:Y.XmlElement|Y.XmlText|null
nextSibling:Y.XmlElement|Y.XmlText|null
prevSibling:Y.XmlElement|Y.XmlText|null
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number)
get(index:number)
length:number
setAttribute(attributeName:string, attributeValue:string)
removeAttribute(attributeName:string)
getAttribute(attributeName:string):string
getAttributes():Object<string,string>
get(i:number):Y.XmlElement|Y.XmlText
Retrieve the i-th element.
slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText>
Retrieve a range of content
clone():Y.XmlElement
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
toDOM():Element
Transforms this type and all children to a new DOM element.
toString():string
Get the XML serialization of all descendants.
toJSON():string
See toString.
observe(function(YXmlEvent, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns.
unobserve(function(YXmlEvent, Transaction):void)
Removes an observe event listener from this type.
observeDeep(function(Array<YEvent>, Transaction):void)
Adds an event listener to this type that will be called synchronously every time this type or any of its children is modified. In the case this type is modified in the event listener, the event listener will be called again after the current event listener returns. The event listener receives all Events created by itself or any of its children.
unobserveDeep(function(Array<YEvent>, Transaction):void)
Removes an observeDeep event listener from this type.
### Y.Doc ```js const doc = new Y.Doc() ```
clientID
A unique id that identifies this client. (readonly)
gc
Whether garbage collection is enabled on this doc instance. Set `doc.gc = false` in order to disable gc and be able to restore old content. See https://github.com/yjs/yjs#yjs-crdt-algorithm for more information about gc in Yjs.
transact(function(Transaction):void [, origin:any])
Every change on the shared document happens in a transaction. Observer calls and the update event are called after each transaction. You should bundle changes into a single transaction to reduce the amount of event calls. I.e. doc.transact(() => { yarray.insert(..); ymap.set(..) }) triggers a single change event.
You can specify an optional origin parameter that is stored on transaction.origin and on('update', (update, origin) => ..).
toJSON():any
Deprecated: It is recommended to call toJSON directly on the shared types. Converts the entire document into a js object, recursively traversing each yjs type. Doesn't log types that have not been defined (using ydoc.getType(..)).
get(string, Y.[TypeClass]):[Type]
Define a shared type.
getArray(string):Y.Array
Define a shared Y.Array type. Is equivalent to y.get(string, Y.Array).
getMap(string):Y.Map
Define a shared Y.Map type. Is equivalent to y.get(string, Y.Map).
getText(string):Y.Text
Define a shared Y.Text type. Is equivalent to y.get(string, Y.Text).
getXmlFragment(string):Y.XmlFragment
Define a shared Y.XmlFragment type. Is equivalent to y.get(string, Y.XmlFragment).
on(string, function)
Register an event listener on the shared type
off(string, function)
Unregister an event listener from the shared type
#### Y.Doc Events
on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)
Listen to document updates. Document updates must be transmitted to all other peers. You can apply document updates in any order and multiple times.
on('beforeTransaction', function(Y.Transaction, Y.Doc):void)
Emitted before each transaction.
on('afterTransaction', function(Y.Transaction, Y.Doc):void)
Emitted after each transaction.
on('beforeAllTransactions', function(Y.Doc):void)
Transactions can be nested (e.g. when an event within a transaction calls another transaction). Emitted before the first transaction.
on('afterAllTransactions', function(Y.Doc, Array<Y.Transaction>):void)
Emitted after the last transaction is cleaned up.
### Document Updates Changes on the shared document are encoded into *document updates*. Document updates are *commutative* and *idempotent*. This means that they can be applied in any order and multiple times. #### Example: Listen to update events and apply them on remote client ```js const doc1 = new Y.Doc() const doc2 = new Y.Doc() doc1.on('update', update => { Y.applyUpdate(doc2, update) }) doc2.on('update', update => { Y.applyUpdate(doc1, update) }) // All changes are also applied to the other document doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?']) doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?' ``` Yjs internally maintains a [state vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences. #### Example: Sync two clients by exchanging the complete document structure ```js const state1 = Y.encodeStateAsUpdate(ydoc1) const state2 = Y.encodeStateAsUpdate(ydoc2) Y.applyUpdate(ydoc1, state2) Y.applyUpdate(ydoc2, state1) ``` #### Example: Sync two clients by computing the differences This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can save a lot of bandwidth. ```js const stateVector1 = Y.encodeStateVector(ydoc1) const stateVector2 = Y.encodeStateVector(ydoc2) const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2) const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1) Y.applyUpdate(ydoc1, diff2) Y.applyUpdate(ydoc2, diff1) ``` #### Example: Syncing clients without loading the Y.Doc It is possible to sync clients and compute delta updates without loading the Yjs document to memory. Yjs exposes an API to compute the differences directly on the binary document updates. ```js // encode the current state as a binary buffer let currentState1 = Y.encodeStateAsUpdate(ydoc1) let currentState2 = Y.encodeStateAsUpdate(ydoc2) // now we can continue syncing clients using state vectors without using the Y.Doc ydoc1.destroy() ydoc2.destroy() const stateVector1 = Y.encodeStateVectorFromUpdate(currentState1) const stateVector2 = Y.encodeStateVectorFromUpdate(currentState2) const diff1 = Y.diffUpdate(currentState1, stateVector2) const diff2 = Y.diffUpdate(currentState2, stateVector1) // sync clients currentState1 = Y.mergeUpdates([currentState1, diff2]) currentState1 = Y.mergeUpdates([currentState1, diff1]) ``` #### Obfuscating Updates If one of your users runs into a weird bug (e.g. the rich-text editor throws error messages), then you don't have to request the full document from your user. Instead, they can obfuscate the document (i.e. replace the content with meaningless generated content) before sending it to you. Note that someone might still deduce the type of content by looking at the general structure of the document. But this is much better than requesting the original document. Obfuscated updates contain all the CRDT-related data that is required for merging. So it is safe to merge obfuscated updates. ```javascript const ydoc = new Y.Doc() // perform some changes.. ydoc.getText().insert(0, 'hello world') const update = Y.encodeStateAsUpdate(ydoc) // the below update contains scrambled data const obfuscatedUpdate = Y.obfuscateUpdate(update) const ydoc2 = new Y.Doc() Y.applyUpdate(ydoc2, obfuscatedUpdate) ydoc2.getText().toString() // => "00000000000" ``` #### Using V2 update format Yjs implements two update formats. By default you are using the V1 update format. You can opt-in into the V2 update format wich provides much better compression. It is not yet used by all providers. However, you can already use it if you are building your own provider. All below functions are available with the suffix "V2". E.g. `Y.applyUpdate` β‡’ `Y.applyUpdateV2`. We also support conversion functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`. #### Update API
Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])
Apply a document update on the shared document. Optionally you can specify transactionOrigin that will be stored on transaction.origin and ydoc.on('update', (update, origin) => ..).
Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array
Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.
Y.encodeStateVector(Y.Doc):Uint8Array
Computes the state vector and encodes it into an Uint8Array.
Y.mergeUpdates(Array<Uint8Array>)
Merge several document updates into a single document update while removing duplicate information. The merged document update is always smaller than the separate updates because of the compressed encoding.
Y.encodeStateVectorFromUpdate(Uint8Array): Uint8Array
Computes the state vector from a document update and encodes it into an Uint8Array.
Y.diffUpdate(update: Uint8Array, stateVector: Uint8Array): Uint8Array
Encode the missing differences to another update message. This function works similarly to Y.encodeStateAsUpdate(ydoc, stateVector) but works on updates instead.
convertUpdateFormatV1ToV2
Convert V1 update format to the V2 update format.
convertUpdateFormatV2ToV1
Convert V2 update format to the V1 update format.
### Relative Positions When working with collaborative documents, we often need to work with positions. Positions may represent cursor locations, selection ranges, or even assign a comment to a range of text. Normal index-positions (expressed as integers) are not convenient to use because the index-range is invalidated as soon as a remote change manipulates the document. Relative positions give you a powerful API to express positions. A relative position is fixated to an element in the shared document and is not affected by remote changes. I.e. given the document `"a|c"`, the relative position is attached to `c`. When a remote user modifies the document by inserting a character before the cursor, the cursor will stay attached to the character `c`. `insert(1, 'x')("a|c") = "ax|c"`. When the relative position is set to the end of the document, it will stay attached to the end of the document. #### Example: Transform to RelativePosition and back ```js const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2) const pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc) pos.type === ytext // => true pos.index === 2 // => true ``` #### Example: Send relative position to remote client (json) ```js const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2) const encodedRelPos = JSON.stringify(relPos) // send encodedRelPos to remote client.. const parsedRelPos = JSON.parse(encodedRelPos) const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc) pos.type === remoteytext // => true pos.index === 2 // => true ``` #### Example: Send relative position to remote client (Uint8Array) ```js const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2) const encodedRelPos = Y.encodeRelativePosition(relPos) // send encodedRelPos to remote client.. const parsedRelPos = Y.decodeRelativePosition(encodedRelPos) const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc) pos.type === remoteytext // => true pos.index === 2 // => true ```
Y.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number [, assoc=0])
Create a relative position fixated to the i-th element in any sequence-like shared type (if assoc >= 0). By default, the position associates with the character that comes after the specified index position. If assoc < 0, then the relative position associates with the character before the specified index position.
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc): { type: Y.AbstractType, index: number, assoc: number } | null
Create an absolute position from a relative position. If the relative position cannot be referenced, or the type is deleted, then the result is null.
Y.encodeRelativePosition(RelativePosition):Uint8Array
Encode a relative position to an Uint8Array. Binary data is the preferred encoding format for document updates. If you prefer JSON encoding, you can simply JSON.stringify / JSON.parse the relative position instead.
Y.decodeRelativePosition(Uint8Array):RelativePosition
Decode a binary-encoded relative position to a RelativePositon object.
### Y.UndoManager Yjs ships with an Undo/Redo manager for selective undo/redo of changes on a Yjs type. The changes can be optionally scoped to transaction origins. ```js const ytext = doc.getText('text') const undoManager = new Y.UndoManager(ytext) ytext.insert(0, 'abc') undoManager.undo() ytext.toString() // => '' undoManager.redo() ytext.toString() // => 'abc' ```
constructor(scope:Y.AbstractType|Array<Y.AbstractType> [, {captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}])
Accepts either single type as scope or an array of types.
undo()
redo()
stopCapturing()
on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when a StackItem is added to the undo- or the redo-stack.
on('stack-item-updated', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when an existing StackItem is updated. This happens when two changes happen within a "captureInterval".
on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo' | 'redo' })
Register an event that is called when a StackItem is popped from the undo- or the redo-stack.
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
Register an event that is called when the undo- and/or the redo-stack is cleared.
#### Example: Stop Capturing UndoManager merges Undo-StackItems if they are created within time-gap smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next StackItem won't be merged. ```js // without stopCapturing ytext.insert(0, 'a') ytext.insert(1, 'b') undoManager.undo() ytext.toString() // => '' (note that 'ab' was removed) // with stopCapturing ytext.insert(0, 'a') undoManager.stopCapturing() ytext.insert(0, 'b') undoManager.undo() ytext.toString() // => 'a' (note that only 'b' was removed) ``` #### Example: Specify tracked origins Every change on the shared document has an origin. If no origin was specified, it defaults to `null`. By specifying `trackedOrigins` you can selectively specify which changes should be tracked by `UndoManager`. The UndoManager instance is always added to `trackedOrigins`. ```js class CustomBinding {} const ytext = doc.getText('text') const undoManager = new Y.UndoManager(ytext, { trackedOrigins: new Set([42, CustomBinding]) }) ytext.insert(0, 'abc') undoManager.undo() ytext.toString() // => 'abc' (does not track because origin `null` and not part // of `trackedTransactionOrigins`) ytext.delete(0, 3) // revert change doc.transact(() => { ytext.insert(0, 'abc') }, 42) undoManager.undo() ytext.toString() // => '' (tracked because origin is an instance of `trackedTransactionorigins`) doc.transact(() => { ytext.insert(0, 'abc') }, 41) undoManager.undo() ytext.toString() // => '' (not tracked because 41 is not an instance of // `trackedTransactionorigins`) ytext.delete(0, 3) // revert change doc.transact(() => { ytext.insert(0, 'abc') }, new CustomBinding()) undoManager.undo() ytext.toString() // => '' (tracked because origin is a `CustomBinding` and // `CustomBinding` is in `trackedTransactionorigins`) ``` #### Example: Add additional information to the StackItems When undoing or redoing a previous action, it is often expected to restore additional meta information like the cursor location or the view on the document. You can assign meta-information to Undo-/Redo-StackItems. ```js const ytext = doc.getText('text') const undoManager = new Y.UndoManager(ytext, { trackedOrigins: new Set([42, CustomBinding]) }) undoManager.on('stack-item-added', event => { // save the current cursor location on the stack-item event.stackItem.meta.set('cursor-location', getRelativeCursorLocation()) }) undoManager.on('stack-item-popped', event => { // restore the current cursor location on the stack-item restoreCursorLocation(event.stackItem.meta.get('cursor-location')) }) ``` ## Yjs CRDT Algorithm *Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differentiation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use mathematical models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth. Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). This [article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/) explains a simple optimization on the CRDT model and gives more insight about the performance characteristics in Yjs. More information about the specific implementation is available in [INTERNALS.md](./INTERNALS.md) and in [this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4). CRDTs that are suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted). **Examples:** 1. If a user inserts elements in sequence, the struct will be merged into a single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is first represented as two structs (`[{id: {client, clock: 0}, content: 'a'}, {id: {client, clock: 1}, content: 'b'}`) and then merged into a single struct: `[{id: {client, clock: 0}, content: 'ab'}]`. 2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore. 3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent. Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](https://github.com/dmonad/crdt-benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks show that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents. ### State Vector Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality. ## License and Author Yjs and all related projects are [**MIT licensed**](./LICENSE). Yjs is based on my research as a student at the [RWTH i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time. Fund this project by donating on [GitHub Sponsors](https://github.com/sponsors/dmonad) or hiring [me](https://github.com/dmonad) as a contractor for your collaborative app. yjs-13.6.8/package-lock.json000066400000000000000000004614011450200430400156300ustar00rootroot00000000000000{ "name": "yjs", "version": "13.6.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yjs", "version": "13.6.8", "license": "MIT", "dependencies": { "lib0": "^0.2.74" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@types/node": "^18.15.5", "concurrently": "^3.6.1", "http-server": "^0.12.3", "jsdoc": "^3.6.7", "markdownlint-cli": "^0.23.2", "rollup": "^3.20.0", "standard": "^16.0.4", "tui-jsdoc-template": "^1.2.2", "typescript": "^4.9.5", "y-protocols": "^1.0.5" }, "engines": { "node": ">=16.0.0", "npm": ">=8.0.0" }, "funding": { "type": "GitHub Sponsors ❀", "url": "https://github.com/sponsors/dmonad" } }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.21.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@eslint/eslintrc": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", "integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.1.1", "espree": "^7.3.0", "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", "js-yaml": "^3.13.1", "lodash": "^4.17.20", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^10.12.0 || >=12.0.0" } }, "node_modules/@eslint/eslintrc/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, "node_modules/@rollup/plugin-commonjs": { "version": "24.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz", "integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "glob": "^8.0.3", "is-reference": "1.2.1", "magic-string": "^0.27.0" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0" }, "peerDependenciesMeta": { "rollup": { "optional": true } } }, "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@rollup/plugin-commonjs/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" } }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-builtin-module": "^3.2.0", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0" }, "peerDependenciesMeta": { "rollup": { "optional": true } } }, "node_modules/@rollup/pluginutils": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^2.3.1" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0" }, "peerDependenciesMeta": { "rollup": { "optional": true } } }, "node_modules/@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, "node_modules/@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", "dev": true }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" } }, "node_modules/@types/mdurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, "node_modules/@types/node": { "version": "18.15.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.5.tgz", "integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==", "dev": true }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-includes": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4", "get-intrinsic": "^1.1.3", "is-string": "^1.0.7" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flat": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flatmap": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "dependencies": { "lodash": "^4.17.14" } }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "node_modules/basic-auth": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", "integrity": "sha512-CtGuTyWf3ig+sgRyC7uP6DM3N+5ur/p8L+FPfsd+BbIfIs74TFfCajZTHnCw6K5dqM0bZEbRIqRy1fAdiUJhTA==", "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", "dev": true, "dependencies": { "lodash": "^4.17.15" }, "engines": { "node": ">= 10" } }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/chalk/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/chalk/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/cheerio": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "integrity": "sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==", "dev": true, "dependencies": { "css-select": "~1.2.0", "dom-serializer": "~0.1.0", "entities": "~1.1.1", "htmlparser2": "^3.9.1", "lodash.assignin": "^4.0.9", "lodash.bind": "^4.1.4", "lodash.defaults": "^4.0.1", "lodash.filter": "^4.4.0", "lodash.flatten": "^4.2.0", "lodash.foreach": "^4.3.0", "lodash.map": "^4.4.0", "lodash.merge": "^4.4.0", "lodash.pick": "^4.2.1", "lodash.reduce": "^4.4.0", "lodash.reject": "^4.4.0", "lodash.some": "^4.4.0" }, "engines": { "node": ">= 0.6" } }, "node_modules/cheerio/node_modules/entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, "engines": { "node": ">=0.1.90" } }, "node_modules/commander": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", "integrity": "sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg==", "dev": true, "engines": { "node": ">= 0.6.x" } }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/concurrently": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.6.1.tgz", "integrity": "sha512-/+ugz+gwFSEfTGUxn0KHkY+19XPRTXR8+7oUK/HxgiN1n7FjeJmkrbSiXAJfyQ0zORgJYPaenmymwon51YXH9Q==", "dev": true, "dependencies": { "chalk": "^2.4.1", "commander": "2.6.0", "date-fns": "^1.23.0", "lodash": "^4.5.1", "read-pkg": "^3.0.0", "rx": "2.3.24", "spawn-command": "^0.0.2-1", "supports-color": "^3.2.3", "tree-kill": "^1.1.0" }, "bin": { "concurrent": "src/main.js", "concurrently": "src/main.js" }, "engines": { "node": ">=4.0.0" } }, "node_modules/corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", "dev": true, "engines": { "node": ">= 0.4.0" } }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==", "dev": true, "dependencies": { "boolbase": "~1.0.0", "css-what": "2.1", "domutils": "1.5.1", "nth-check": "~1.0.1" } }, "node_modules/css-what": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true, "engines": { "node": "*" } }, "node_modules/date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", "dev": true }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/deep-extend": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "dependencies": { "esutils": "^2.0.2" }, "engines": { "node": ">=6.0.0" } }, "node_modules/dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", "dev": true, "dependencies": { "domelementtype": "^1.3.0", "entities": "^1.1.1" } }, "node_modules/dom-serializer/node_modules/entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, "node_modules/domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true }, "node_modules/domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "dev": true, "dependencies": { "domelementtype": "1" } }, "node_modules/domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", "dev": true, "dependencies": { "dom-serializer": "0", "domelementtype": "1" } }, "node_modules/ecstatic": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz", "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==", "deprecated": "This package is unmaintained and deprecated. See the GH Issue 259.", "dev": true, "dependencies": { "he": "^1.1.1", "mime": "^1.6.0", "minimist": "^1.1.0", "url-join": "^2.0.5" }, "bin": { "ecstatic": "lib/ecstatic.js" } }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, "dependencies": { "ansi-colors": "^4.1.1" }, "engines": { "node": ">=8.6" } }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.2.0", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.9" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", "dev": true, "dependencies": { "get-intrinsic": "^1.1.3", "has": "^1.0.3", "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", "dev": true, "dependencies": { "has": "^1.0.3" } }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", "is-symbol": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { "node": ">=0.8.0" } }, "node_modules/eslint": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash": "^4.17.20", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "progress": "^2.0.0", "regexpp": "^3.1.0", "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", "table": "^6.0.4", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { "node": "^10.12.0 || >=12.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-standard": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz", "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "peerDependencies": { "eslint": "^7.12.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1 || ^5.0.0" } }, "node_modules/eslint-config-standard-jsx": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-10.0.0.tgz", "integrity": "sha512-hLeA2f5e06W1xyr/93/QJulN/rLbUVUmqTlexv9PRKHFwEC9ffJcH2LvJhMoEqYQBEYafedgGZXH2W8NUpt5lA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "peerDependencies": { "eslint": "^7.12.1", "eslint-plugin-react": "^7.21.5" } }, "node_modules/eslint-import-resolver-node": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", "dev": true, "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.11.0", "resolve": "^1.22.1" } }, "node_modules/eslint-module-utils": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", "dev": true, "dependencies": { "debug": "^3.2.7" }, "engines": { "node": ">=4" }, "peerDependenciesMeta": { "eslint": { "optional": true } } }, "node_modules/eslint-plugin-es": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" }, "engines": { "node": ">=8.10.0" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { "eslint": ">=4.19.1" } }, "node_modules/eslint-plugin-import": { "version": "2.24.2", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz", "integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==", "dev": true, "dependencies": { "array-includes": "^3.1.3", "array.prototype.flat": "^1.2.4", "debug": "^2.6.9", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.6", "eslint-module-utils": "^2.6.2", "find-up": "^2.0.0", "has": "^1.0.3", "is-core-module": "^2.6.0", "minimatch": "^3.0.4", "object.values": "^1.1.4", "pkg-up": "^2.0.0", "read-pkg-up": "^3.0.0", "resolve": "^1.20.0", "tsconfig-paths": "^3.11.0" }, "engines": { "node": ">=4" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0" } }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "dependencies": { "esutils": "^2.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/eslint-plugin-import/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", "ignore": "^5.1.1", "minimatch": "^3.0.4", "resolve": "^1.10.1", "semver": "^6.1.0" }, "engines": { "node": ">=8.10.0" }, "peerDependencies": { "eslint": ">=5.16.0" } }, "node_modules/eslint-plugin-node/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-promise": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz", "integrity": "sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA==", "dev": true, "engines": { "node": "^10.12.0 || >=12.0.0" }, "peerDependencies": { "eslint": "^7.0.0" } }, "node_modules/eslint-plugin-react": { "version": "7.25.3", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.3.tgz", "integrity": "sha512-ZMbFvZ1WAYSZKY662MBVEWR45VaBT6KSJCiupjrNlcdakB90juaZeDCbJq19e73JZQubqFtgETohwgAt8u5P6w==", "dev": true, "dependencies": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", "doctrine": "^2.1.0", "estraverse": "^5.2.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.0.4", "object.entries": "^1.1.4", "object.fromentries": "^2.0.4", "object.hasown": "^1.0.0", "object.values": "^1.1.4", "prop-types": "^15.7.2", "resolve": "^2.0.0-next.3", "string.prototype.matchall": "^4.0.5" }, "engines": { "node": ">=4" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "dependencies": { "esutils": "^2.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", "dev": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" }, "engines": { "node": ">=8.0.0" } }, "node_modules/eslint-scope/node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "engines": { "node": ">=4.0" } }, "node_modules/eslint-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "dependencies": { "eslint-visitor-keys": "^1.1.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, "engines": { "node": ">=10" } }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/eslint/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "node_modules/eslint/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/eslint/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/eslint/node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/espree": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "dependencies": { "acorn": "^7.4.0", "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^1.3.0" }, "engines": { "node": "^10.12.0 || >=12.0.0" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" }, "engines": { "node": ">=4" } }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" }, "engines": { "node": ">=0.10" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "dependencies": { "estraverse": "^5.2.0" }, "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, "engines": { "node": "^10.12.0 || >=12.0.0" } }, "node_modules/find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, "dependencies": { "locate-path": "^2.0.0" }, "engines": { "node": ">=4" } }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "dependencies": { "flatted": "^3.1.0", "rimraf": "^3.0.2" }, "engines": { "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true, "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], "engines": { "node": ">=4.0" }, "peerDependenciesMeta": { "debug": { "optional": true } } }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "dependencies": { "is-callable": "^1.1.3" } }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, "node_modules/function.prototype.name": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", "es-abstract": "^1.19.0", "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", "integrity": "sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==", "dev": true, "engines": { "node": ">=0.12.0" } }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { "is-glob": "^4.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "dependencies": { "type-fest": "^0.8.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, "dependencies": { "define-properties": "^1.1.3" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", "dev": true }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "dependencies": { "function-bind": "^1.1.1" }, "engines": { "node": ">= 0.4.0" } }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", "dev": true, "dependencies": { "get-intrinsic": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, "bin": { "he": "bin/he" } }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "node_modules/htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "dev": true, "dependencies": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", "domutils": "^1.5.1", "entities": "^1.1.1", "inherits": "^2.0.1", "readable-stream": "^3.1.1" } }, "node_modules/htmlparser2/node_modules/entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/http-server": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz", "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==", "dev": true, "dependencies": { "basic-auth": "^1.0.3", "colors": "^1.4.0", "corser": "^2.0.1", "ecstatic": "^3.3.2", "http-proxy": "^1.18.0", "minimist": "^1.2.5", "opener": "^1.5.1", "portfinder": "^1.0.25", "secure-compare": "3.0.1", "union": "~0.5.0" }, "bin": { "hs": "bin/http-server", "http-server": "bin/http-server" }, "engines": { "node": ">=6" } }, "node_modules/ignore": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { "node": ">=0.8.19" } }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" }, "engines": { "node": ">= 0.4" } }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "dependencies": { "builtin-modules": "^3.3.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "dependencies": { "has": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "dev": true }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "dependencies": { "@types/estree": "*" } }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, "dependencies": { "call-bind": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-typed-array": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "node_modules/isomorphic.js": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "funding": { "type": "GitHub Sponsors ❀", "url": "https://github.com/sponsors/dmonad" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/js-yaml/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/js2xmlparser": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", "dev": true, "dependencies": { "xmlcreate": "^2.0.4" } }, "node_modules/jsdoc": { "version": "3.6.11", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", "dev": true, "dependencies": { "@babel/parser": "^7.9.4", "@types/markdown-it": "^12.2.3", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", "js2xmlparser": "^4.0.2", "klaw": "^3.0.0", "markdown-it": "^12.3.2", "markdown-it-anchor": "^8.4.1", "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "taffydb": "2.6.2", "underscore": "~1.13.2" }, "bin": { "jsdoc": "jsdoc.js" }, "engines": { "node": ">=12.0.0" } }, "node_modules/jsdoc/node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "node_modules/jsonc-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", "dev": true }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", "dev": true, "dependencies": { "array-includes": "^3.1.5", "object.assign": "^4.1.3" }, "engines": { "node": ">=4.0" } }, "node_modules/klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", "dev": true, "dependencies": { "graceful-fs": "^4.1.9" } }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/lib0": { "version": "0.2.74", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.74.tgz", "integrity": "sha512-roj9i46/JwG5ik5KNTkxP2IytlnrssAkD/OhlAVtE+GqectrdkfR+pttszVLrOzMDeXNs1MPt6yo66MUolWSiA==", "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0gentesthtml": "bin/gentesthtml.js", "0serve": "bin/0serve.js" }, "engines": { "node": ">=14" }, "funding": { "type": "GitHub Sponsors ❀", "url": "https://github.com/sponsors/dmonad" } }, "node_modules/linkify-it": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "node_modules/lodash.assignin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", "integrity": "sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==", "dev": true }, "node_modules/lodash.bind": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", "integrity": "sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==", "dev": true }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true }, "node_modules/lodash.differencewith": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz", "integrity": "sha512-/8JFjydAS+4bQuo3CpLMBv7WxGFyk7/etOAsrQUCu0a9QVDemxv0YQ0rFyeZvqlUD314SERfNlgnlqqHmaQ0Cg==", "dev": true }, "node_modules/lodash.filter": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", "dev": true }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, "node_modules/lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", "dev": true }, "node_modules/lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "node_modules/lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", "dev": true }, "node_modules/lodash.reduce": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", "dev": true }, "node_modules/lodash.reject": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", "integrity": "sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==", "dev": true }, "node_modules/lodash.some": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", "integrity": "sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", "dev": true }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, "node_modules/magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.13" }, "engines": { "node": ">=12" } }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "bin": { "markdown-it": "bin/markdown-it.js" } }, "node_modules/markdown-it-anchor": { "version": "8.6.7", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", "dev": true, "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "node_modules/markdownlint": { "version": "0.20.4", "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.20.4.tgz", "integrity": "sha512-jpfaPgjT0OpeBbemjYNZbzGG3hCLcAIvrm/pEY3+q/szDScG6ZonDacqySVRJAv9glbo8y4wBPJ0wgW17+9GGA==", "dev": true, "dependencies": { "markdown-it": "10.0.0" }, "engines": { "node": ">=10" } }, "node_modules/markdownlint-cli": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.23.2.tgz", "integrity": "sha512-OSl5OZ8xzGN6z355cqRkiq67zPi3reJimklaF72p0554q85Dng5ToOjjSB9tDKZebSt85jX8cp+ruoQlPqOsPA==", "dev": true, "dependencies": { "commander": "~2.9.0", "deep-extend": "~0.5.1", "get-stdin": "~5.0.1", "glob": "~7.1.2", "ignore": "~5.1.4", "js-yaml": "~3.13.1", "jsonc-parser": "~2.2.0", "lodash.differencewith": "~4.5.0", "lodash.flatten": "~4.4.0", "markdownlint": "~0.20.4", "markdownlint-rule-helpers": "~0.11.0", "minimatch": "~3.0.4", "minimist": "~1.2.5", "rc": "~1.2.7" }, "bin": { "markdownlint": "markdownlint.js" }, "engines": { "node": ">=10" } }, "node_modules/markdownlint-cli/node_modules/commander": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", "dev": true, "dependencies": { "graceful-readlink": ">= 1.0.0" }, "engines": { "node": ">= 0.6.x" } }, "node_modules/markdownlint-cli/node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/markdownlint-cli/node_modules/minimatch": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/markdownlint-rule-helpers": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.11.0.tgz", "integrity": "sha512-PhGii9dOiDJDXxiRMpK8N0FM9powprvRPsXALgkjlSPTwLh6ymH+iF3iUe3nq8KGu26tclFBlLL5xAGy/zb7FA==", "dev": true }, "node_modules/markdownlint/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/markdownlint/node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "dev": true }, "node_modules/markdownlint/node_modules/linkify-it": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", "dev": true, "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/markdownlint/node_modules/markdown-it": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", "dev": true, "dependencies": { "argparse": "^1.0.7", "entities": "~2.0.0", "linkify-it": "^2.0.0", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "bin": { "markdown-it": "bin/markdown-it.js" } }, "node_modules/marked": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", "dev": true, "bin": { "marked": "bin/marked.js" }, "engines": { "node": ">= 12" } }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "dev": true }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "bin": { "mime": "cli.js" }, "engines": { "node": ">=4" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, "engines": { "node": ">=10" } }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "node_modules/nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "dev": true, "dependencies": { "boolbase": "~1.0.0" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.entries": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.hasown": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", "dev": true, "dependencies": { "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" } }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, "bin": { "opener": "bin/opener-bin.js" } }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.3" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "dependencies": { "p-try": "^1.0.0" }, "engines": { "node": ">=4" } }, "node_modules/p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, "dependencies": { "p-limit": "^1.1.0" }, "engines": { "node": ">=4" } }, "node_modules/p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "dependencies": { "callsites": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" }, "engines": { "node": ">=4" } }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "dependencies": { "pify": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/pkg-conf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", "dev": true, "dependencies": { "find-up": "^3.0.0", "load-json-file": "^5.2.0" }, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "dependencies": { "locate-path": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/load-json-file": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", "dev": true, "dependencies": { "graceful-fs": "^4.1.15", "parse-json": "^4.0.0", "pify": "^4.0.1", "strip-bom": "^3.0.0", "type-fest": "^0.3.0" }, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-conf/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "dependencies": { "p-limit": "^2.0.0" }, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/pkg-conf/node_modules/type-fest": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/pkg-up": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", "integrity": "sha512-fjAPuiws93rm7mPUu21RdBnkeZNrbfCFCwfAhPWY+rR3zG0ubpe5cEReHOw5fIbfmsxEV/g2kSxGTATY3Bpnwg==", "dev": true, "dependencies": { "find-up": "^2.1.0" }, "engines": { "node": ">=4" } }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", "dev": true, "dependencies": { "async": "^2.6.4", "debug": "^3.2.7", "mkdirp": "^0.5.6" }, "engines": { "node": ">= 0.12.0" } }, "node_modules/portfinder/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "engines": { "node": ">=0.4.0" } }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/qs": { "version": "6.11.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", "dev": true, "dependencies": { "side-channel": "^1.0.4" }, "engines": { "node": ">=0.6" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "cli.js" } }, "node_modules/rc/node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "engines": { "node": ">=4.0.0" } }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/read-pkg-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", "dev": true, "dependencies": { "find-up": "^2.0.0", "read-pkg": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "node_modules/requizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", "dev": true, "dependencies": { "lodash": "^4.17.21" } }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dev": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rollup": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.0.tgz", "integrity": "sha512-YsIfrk80NqUDrxrjWPXUa7PWvAfegZEXHuPsEZg58fGCdjL1I9C1i/NaG+L+27kxxwkrG/QEDEQc8s/ynXWWGQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { "node": ">=14.18.0", "npm": ">=8.0.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/rx": { "version": "2.3.24", "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", "integrity": "sha512-Ue4ZB7Dzbn2I9sIj8ws536nOP2S53uypyCkCz9q0vlYD5Kn6/pu4dE+wt2ZfFzd9m73hiYKnnCb1OyKqc+MRkg==", "dev": true }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "is-regex": "^1.1.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", "dev": true }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, "bin": { "semver": "bin/semver" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "node_modules/spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", "dev": true }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { "version": "3.0.13", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "node_modules/standard": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/standard/-/standard-16.0.4.tgz", "integrity": "sha512-2AGI874RNClW4xUdM+bg1LRXVlYLzTNEkHmTG5mhyn45OhbgwA+6znowkOGYy+WMb5HRyELvtNy39kcdMQMcYQ==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "dependencies": { "eslint": "~7.18.0", "eslint-config-standard": "16.0.3", "eslint-config-standard-jsx": "10.0.0", "eslint-plugin-import": "~2.24.2", "eslint-plugin-node": "~11.1.0", "eslint-plugin-promise": "~5.1.0", "eslint-plugin-react": "~7.25.1", "standard-engine": "^14.0.1" }, "bin": { "standard": "bin/cmd.js" }, "engines": { "node": ">=10.12.0" } }, "node_modules/standard-engine": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-14.0.1.tgz", "integrity": "sha512-7FEzDwmHDOGva7r9ifOzD3BGdTbA7ujJ50afLVdW/tK14zQEptJjbFuUfn50irqdHDcTbNh0DTIoMPynMCXb0Q==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "dependencies": { "get-stdin": "^8.0.0", "minimist": "^1.2.5", "pkg-conf": "^3.1.0", "xdg-basedir": "^4.0.0" }, "engines": { "node": ">=8.10" } }, "node_modules/standard-engine/node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4", "get-intrinsic": "^1.1.3", "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", "regexp.prototype.flags": "^1.4.3", "side-channel": "^1.0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", "es-abstract": "^1.20.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", "dev": true, "dependencies": { "has-flag": "^1.0.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/table/node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "node_modules/taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", "dev": true }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "bin": { "tree-kill": "cli.js" } }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "node_modules/tui-jsdoc-template": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tui-jsdoc-template/-/tui-jsdoc-template-1.2.2.tgz", "integrity": "sha512-oqw0IYaot86VJ2owKBozJnilgta0Z55x8r9PeHj7vb+jDoSvJGRUQUcgs56SZh9HE20fx54Pe75p84X85/ygLA==", "dev": true, "dependencies": { "cheerio": "^0.22.0" } }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", "is-typed-array": "^1.1.9" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=4.2.0" } }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/underscore": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", "dev": true, "dependencies": { "qs": "^6.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "dependencies": { "punycode": "^2.1.0" } }, "node_modules/url-join": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", "integrity": "sha512-c2H1fIgpUdwFRIru9HFno5DT73Ok8hg5oOb5AT3ayIgvCRfxgs2jyt5Slw8kEB7j3QUr6yJmMPDT/odjk7jXow==", "dev": true }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0", "is-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "node_modules/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "dev": true }, "node_modules/y-protocols": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.5.tgz", "integrity": "sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==", "dev": true, "dependencies": { "lib0": "^0.2.42" }, "funding": { "type": "GitHub Sponsors ❀", "url": "https://github.com/sponsors/dmonad" } }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } } yjs-13.6.8/package.json000066400000000000000000000054421450200430400147010ustar00rootroot00000000000000{ "name": "yjs", "version": "13.6.8", "description": "Shared Editing Library", "main": "./dist/yjs.cjs", "module": "./dist/yjs.mjs", "types": "./dist/src/index.d.ts", "type": "module", "sideEffects": false, "funding": { "type": "GitHub Sponsors ❀", "url": "https://github.com/sponsors/dmonad" }, "scripts": { "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000", "dist": "rm -rf dist && rollup -c && tsc", "watch": "rollup -wc", "lint": "markdownlint README.md && standard && tsc", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "serve-docs": "npm run docs && http-server ./docs/", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs", "debug": "concurrently 'http-server -o test.html' 'npm run watch'", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs", "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs" }, "exports": { ".": { "types": "./dist/src/index.d.ts", "module": "./dist/yjs.mjs", "import": "./dist/yjs.mjs", "require": "./dist/yjs.cjs" }, "./src/index.js": "./src/index.js", "./tests/testHelper.js": "./tests/testHelper.js", "./testHelper": "./dist/testHelper.mjs", "./package.json": "./package.json" }, "files": [ "dist/yjs.*", "dist/src", "src", "tests/testHelper.js", "dist/testHelper.mjs", "sponsor-y.js" ], "dictionaries": { "test": "tests" }, "standard": { "ignore": [ "/dist", "/node_modules", "/docs" ] }, "repository": { "type": "git", "url": "https://github.com/yjs/yjs.git" }, "keywords": [ "Yjs", "CRDT", "offline", "offline-first", "shared-editing", "concurrency", "collaboration" ], "author": "Kevin Jahns", "email": "kevin.jahns@protonmail.com", "license": "MIT", "bugs": { "url": "https://github.com/yjs/yjs/issues" }, "homepage": "https://docs.yjs.dev", "dependencies": { "lib0": "^0.2.74" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@types/node": "^18.15.5", "concurrently": "^3.6.1", "http-server": "^0.12.3", "jsdoc": "^3.6.7", "markdownlint-cli": "^0.23.2", "rollup": "^3.20.0", "standard": "^16.0.4", "tui-jsdoc-template": "^1.2.2", "typescript": "^4.9.5", "y-protocols": "^1.0.5" }, "engines": { "npm": ">=8.0.0", "node": ">=16.0.0" } } yjs-13.6.8/rollup.config.js000066400000000000000000000041041450200430400155240ustar00rootroot00000000000000import nodeResolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' const localImports = process.env.LOCALIMPORTS const customModules = new Set([ 'y-websocket', 'y-codemirror', 'y-ace', 'y-textarea', 'y-quill', 'y-dom', 'y-prosemirror' ]) /** * @type {Set} */ const customLibModules = new Set([ 'lib0', 'y-protocols' ]) const debugResolve = { resolveId (importee) { if (importee === 'yjs') { return `${process.cwd()}/src/index.js` } if (localImports) { if (customModules.has(importee.split('/')[0])) { return `${process.cwd()}/../${importee}/src/${importee}.js` } if (customLibModules.has(importee.split('/')[0])) { return `${process.cwd()}/../${importee}` } } return null } } export default [{ input: './src/index.js', output: { name: 'Y', file: 'dist/yjs.cjs', format: 'cjs', sourcemap: true }, external: id => /^lib0\//.test(id) }, { input: './src/index.js', output: { name: 'Y', file: 'dist/yjs.mjs', format: 'esm', sourcemap: true }, external: id => /^lib0\//.test(id) }, { input: './tests/testHelper.js', output: { name: 'Y', file: 'dist/testHelper.mjs', format: 'esm', sourcemap: true }, external: id => /^lib0\//.test(id) || id === 'yjs', plugins: [{ resolveId (importee) { if (importee === '../src/index.js') { return 'yjs' } return null } }] }, { input: './tests/index.js', output: { name: 'test', file: 'dist/tests.js', format: 'iife', sourcemap: true }, plugins: [ debugResolve, nodeResolve({ mainFields: ['browser', 'module', 'main'] }), commonjs() ] }, { input: './tests/index.js', output: { name: 'test', file: 'dist/tests.cjs', format: 'cjs', sourcemap: true }, plugins: [ debugResolve, nodeResolve({ mainFields: ['node', 'module', 'main'], exportConditions: ['node', 'module', 'import', 'default'] }), commonjs() ], external: id => /^lib0\//.test(id) }] yjs-13.6.8/src/000077500000000000000000000000001450200430400131755ustar00rootroot00000000000000yjs-13.6.8/src/index.js000066400000000000000000000060261450200430400146460ustar00rootroot00000000000000/** eslint-env browser */ export { Doc, Transaction, YArray as Array, YMap as Map, YText as Text, YXmlText as XmlText, YXmlHook as XmlHook, YXmlElement as XmlElement, YXmlFragment as XmlFragment, YXmlEvent, YMapEvent, YArrayEvent, YTextEvent, YEvent, Item, AbstractStruct, GC, ContentBinary, ContentDeleted, ContentEmbed, ContentFormat, ContentJSON, ContentAny, ContentString, ContentType, AbstractType, getTypeChildren, createRelativePositionFromTypeIndex, createRelativePositionFromJSON, createAbsolutePositionFromRelativePosition, compareRelativePositions, AbsolutePosition, RelativePosition, ID, createID, compareIDs, getState, Snapshot, createSnapshot, createDeleteSet, createDeleteSetFromStructStore, cleanupYTextFormatting, snapshot, emptySnapshot, findRootTypeKey, findIndexSS, getItem, typeListToArraySnapshot, typeMapGetSnapshot, createDocFromSnapshot, iterateDeletedStructs, applyUpdate, applyUpdateV2, readUpdate, readUpdateV2, encodeStateAsUpdate, encodeStateAsUpdateV2, encodeStateVector, UndoManager, decodeSnapshot, encodeSnapshot, decodeSnapshotV2, encodeSnapshotV2, decodeStateVector, logUpdate, logUpdateV2, decodeUpdate, decodeUpdateV2, relativePositionToJSON, isDeleted, isParentOf, equalSnapshots, PermanentUserData, // @TODO experimental tryGc, transact, AbstractConnector, logType, mergeUpdates, mergeUpdatesV2, parseUpdateMeta, parseUpdateMetaV2, encodeStateVectorFromUpdate, encodeStateVectorFromUpdateV2, encodeRelativePosition, decodeRelativePosition, diffUpdate, diffUpdateV2, convertUpdateFormatV1ToV2, convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, UpdateEncoderV1, equalDeleteSets, snapshotContainsUpdate } from './internals.js' const glo = /** @type {any} */ (typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window // @ts-ignore : typeof global !== 'undefined' ? global : {}) const importIdentifier = '__ $YJS$ __' if (glo[importIdentifier] === true) { /** * Dear reader of this message. Please take this seriously. * * If you see this message, make sure that you only import one version of Yjs. In many cases, * your package manager installs two versions of Yjs that are used by different packages within your project. * Another reason for this message is that some parts of your project use the commonjs version of Yjs * and others use the EcmaScript version of Yjs. * * This often leads to issues that are hard to debug. We often need to perform constructor checks, * e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to * do the constructor checks anymore - which might break the CRDT algorithm. * * https://github.com/yjs/yjs/issues/438 */ console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438') } glo[importIdentifier] = true yjs-13.6.8/src/internals.js000066400000000000000000000030001450200430400155230ustar00rootroot00000000000000 export * from './utils/AbstractConnector.js' export * from './utils/DeleteSet.js' export * from './utils/Doc.js' export * from './utils/UpdateDecoder.js' export * from './utils/UpdateEncoder.js' export * from './utils/encoding.js' export * from './utils/EventHandler.js' export * from './utils/ID.js' export * from './utils/isParentOf.js' export * from './utils/logging.js' export * from './utils/PermanentUserData.js' export * from './utils/RelativePosition.js' export * from './utils/Snapshot.js' export * from './utils/StructStore.js' export * from './utils/Transaction.js' export * from './utils/UndoManager.js' export * from './utils/updates.js' export * from './utils/YEvent.js' export * from './types/AbstractType.js' export * from './types/YArray.js' export * from './types/YMap.js' export * from './types/YText.js' export * from './types/YXmlFragment.js' export * from './types/YXmlElement.js' export * from './types/YXmlEvent.js' export * from './types/YXmlHook.js' export * from './types/YXmlText.js' export * from './structs/AbstractStruct.js' export * from './structs/GC.js' export * from './structs/ContentBinary.js' export * from './structs/ContentDeleted.js' export * from './structs/ContentDoc.js' export * from './structs/ContentEmbed.js' export * from './structs/ContentFormat.js' export * from './structs/ContentJSON.js' export * from './structs/ContentAny.js' export * from './structs/ContentString.js' export * from './structs/ContentType.js' export * from './structs/Item.js' export * from './structs/Skip.js' yjs-13.6.8/src/structs/000077500000000000000000000000001450200430400147045ustar00rootroot00000000000000yjs-13.6.8/src/structs/AbstractStruct.js000066400000000000000000000022101450200430400202050ustar00rootroot00000000000000 import { UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' export class AbstractStruct { /** * @param {ID} id * @param {number} length */ constructor (id, length) { this.id = id this.length = length } /** * @type {boolean} */ get deleted () { throw error.methodUnimplemented() } /** * Merge this struct with the item to the right. * This method is already assuming that `this.id.clock + this.length === this.id.clock`. * Also this method does *not* remove right from StructStore! * @param {AbstractStruct} right * @return {boolean} wether this merged with right */ mergeWith (right) { return false } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. * @param {number} offset * @param {number} encodingRef */ write (encoder, offset, encodingRef) { throw error.methodUnimplemented() } /** * @param {Transaction} transaction * @param {number} offset */ integrate (transaction, offset) { throw error.methodUnimplemented() } } yjs-13.6.8/src/structs/ContentAny.js000066400000000000000000000035371450200430400173340ustar00rootroot00000000000000import { UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line } from '../internals.js' export class ContentAny { /** * @param {Array} arr */ constructor (arr) { /** * @type {Array} */ this.arr = arr } /** * @return {number} */ getLength () { return this.arr.length } /** * @return {Array} */ getContent () { return this.arr } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentAny} */ copy () { return new ContentAny(this.arr) } /** * @param {number} offset * @return {ContentAny} */ splice (offset) { const right = new ContentAny(this.arr.slice(offset)) this.arr = this.arr.slice(0, offset) return right } /** * @param {ContentAny} right * @return {boolean} */ mergeWith (right) { this.arr = this.arr.concat(right.arr) return true } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) {} /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { const len = this.arr.length encoder.writeLen(len - offset) for (let i = offset; i < len; i++) { const c = this.arr[i] encoder.writeAny(c) } } /** * @return {number} */ getRef () { return 8 } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentAny} */ export const readContentAny = decoder => { const len = decoder.readLen() const cs = [] for (let i = 0; i < len; i++) { cs.push(decoder.readAny()) } return new ContentAny(cs) } yjs-13.6.8/src/structs/ContentBinary.js000066400000000000000000000030211450200430400200150ustar00rootroot00000000000000import { UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' export class ContentBinary { /** * @param {Uint8Array} content */ constructor (content) { this.content = content } /** * @return {number} */ getLength () { return 1 } /** * @return {Array} */ getContent () { return [this.content] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentBinary} */ copy () { return new ContentBinary(this.content) } /** * @param {number} offset * @return {ContentBinary} */ splice (offset) { throw error.methodUnimplemented() } /** * @param {ContentBinary} right * @return {boolean} */ mergeWith (right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) {} /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeBuf(this.content) } /** * @return {number} */ getRef () { return 3 } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder * @return {ContentBinary} */ export const readContentBinary = decoder => new ContentBinary(decoder.readBuf()) yjs-13.6.8/src/structs/ContentDeleted.js000066400000000000000000000032771450200430400201540ustar00rootroot00000000000000 import { addToDeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line } from '../internals.js' export class ContentDeleted { /** * @param {number} len */ constructor (len) { this.len = len } /** * @return {number} */ getLength () { return this.len } /** * @return {Array} */ getContent () { return [] } /** * @return {boolean} */ isCountable () { return false } /** * @return {ContentDeleted} */ copy () { return new ContentDeleted(this.len) } /** * @param {number} offset * @return {ContentDeleted} */ splice (offset) { const right = new ContentDeleted(this.len - offset) this.len = offset return right } /** * @param {ContentDeleted} right * @return {boolean} */ mergeWith (right) { this.len += right.len return true } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len) item.markDeleted() } /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeLen(this.len - offset) } /** * @return {number} */ getRef () { return 1 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder * @return {ContentDeleted} */ export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen()) yjs-13.6.8/src/structs/ContentDoc.js000066400000000000000000000051621450200430400173060ustar00rootroot00000000000000 import { Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' /** * @param {string} guid * @param {Object} opts */ const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false }) /** * @private */ export class ContentDoc { /** * @param {Doc} doc */ constructor (doc) { if (doc._item) { console.error('This document was already integrated as a sub-document. You should create a second instance instead with the same guid.') } /** * @type {Doc} */ this.doc = doc /** * @type {any} */ const opts = {} this.opts = opts if (!doc.gc) { opts.gc = false } if (doc.autoLoad) { opts.autoLoad = true } if (doc.meta !== null) { opts.meta = doc.meta } } /** * @return {number} */ getLength () { return 1 } /** * @return {Array} */ getContent () { return [this.doc] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentDoc} */ copy () { return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts)) } /** * @param {number} offset * @return {ContentDoc} */ splice (offset) { throw error.methodUnimplemented() } /** * @param {ContentDoc} right * @return {boolean} */ mergeWith (right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { // this needs to be reflected in doc.destroy as well this.doc._item = item transaction.subdocsAdded.add(this.doc) if (this.doc.shouldLoad) { transaction.subdocsLoaded.add(this.doc) } } /** * @param {Transaction} transaction */ delete (transaction) { if (transaction.subdocsAdded.has(this.doc)) { transaction.subdocsAdded.delete(this.doc) } else { transaction.subdocsRemoved.add(this.doc) } } /** * @param {StructStore} store */ gc (store) { } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeString(this.doc.guid) encoder.writeAny(this.opts) } /** * @return {number} */ getRef () { return 9 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentDoc} */ export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny())) yjs-13.6.8/src/structs/ContentEmbed.js000066400000000000000000000030341450200430400176110ustar00rootroot00000000000000 import { UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' /** * @private */ export class ContentEmbed { /** * @param {Object} embed */ constructor (embed) { this.embed = embed } /** * @return {number} */ getLength () { return 1 } /** * @return {Array} */ getContent () { return [this.embed] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentEmbed} */ copy () { return new ContentEmbed(this.embed) } /** * @param {number} offset * @return {ContentEmbed} */ splice (offset) { throw error.methodUnimplemented() } /** * @param {ContentEmbed} right * @return {boolean} */ mergeWith (right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) {} /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeJSON(this.embed) } /** * @return {number} */ getRef () { return 5 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentEmbed} */ export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON()) yjs-13.6.8/src/structs/ContentFormat.js000066400000000000000000000034761450200430400200370ustar00rootroot00000000000000 import { YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' /** * @private */ export class ContentFormat { /** * @param {string} key * @param {Object} value */ constructor (key, value) { this.key = key this.value = value } /** * @return {number} */ getLength () { return 1 } /** * @return {Array} */ getContent () { return [] } /** * @return {boolean} */ isCountable () { return false } /** * @return {ContentFormat} */ copy () { return new ContentFormat(this.key, this.value) } /** * @param {number} _offset * @return {ContentFormat} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {ContentFormat} _right * @return {boolean} */ mergeWith (_right) { return false } /** * @param {Transaction} _transaction * @param {Item} item */ integrate (_transaction, item) { // @todo searchmarker are currently unsupported for rich text documents const p = /** @type {YText} */ (item.parent) p._searchMarker = null p._hasFormatting = true } /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeKey(this.key) encoder.writeJSON(this.value) } /** * @return {number} */ getRef () { return 6 } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentFormat} */ export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON()) yjs-13.6.8/src/structs/ContentJSON.js000066400000000000000000000040501450200430400173450ustar00rootroot00000000000000import { UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line } from '../internals.js' /** * @private */ export class ContentJSON { /** * @param {Array} arr */ constructor (arr) { /** * @type {Array} */ this.arr = arr } /** * @return {number} */ getLength () { return this.arr.length } /** * @return {Array} */ getContent () { return this.arr } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentJSON} */ copy () { return new ContentJSON(this.arr) } /** * @param {number} offset * @return {ContentJSON} */ splice (offset) { const right = new ContentJSON(this.arr.slice(offset)) this.arr = this.arr.slice(0, offset) return right } /** * @param {ContentJSON} right * @return {boolean} */ mergeWith (right) { this.arr = this.arr.concat(right.arr) return true } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) {} /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { const len = this.arr.length encoder.writeLen(len - offset) for (let i = offset; i < len; i++) { const c = this.arr[i] encoder.writeString(c === undefined ? 'undefined' : JSON.stringify(c)) } } /** * @return {number} */ getRef () { return 2 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentJSON} */ export const readContentJSON = decoder => { const len = decoder.readLen() const cs = [] for (let i = 0; i < len; i++) { const c = decoder.readString() if (c === 'undefined') { cs.push(undefined) } else { cs.push(JSON.parse(c)) } } return new ContentJSON(cs) } yjs-13.6.8/src/structs/ContentString.js000066400000000000000000000045211450200430400200450ustar00rootroot00000000000000import { UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line } from '../internals.js' /** * @private */ export class ContentString { /** * @param {string} str */ constructor (str) { /** * @type {string} */ this.str = str } /** * @return {number} */ getLength () { return this.str.length } /** * @return {Array} */ getContent () { return this.str.split('') } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentString} */ copy () { return new ContentString(this.str) } /** * @param {number} offset * @return {ContentString} */ splice (offset) { const right = new ContentString(this.str.slice(offset)) this.str = this.str.slice(0, offset) // Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248 const firstCharCode = this.str.charCodeAt(offset - 1) if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) { // Last character of the left split is the start of a surrogate utf16/ucs2 pair. // We don't support splitting of surrogate pairs because this may lead to invalid documents. // Replace the invalid character with a unicode replacement character (οΏ½ / U+FFFD) this.str = this.str.slice(0, offset - 1) + 'οΏ½' // replace right as well right.str = 'οΏ½' + right.str.slice(1) } return right } /** * @param {ContentString} right * @return {boolean} */ mergeWith (right) { this.str += right.str return true } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) {} /** * @param {Transaction} transaction */ delete (transaction) {} /** * @param {StructStore} store */ gc (store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeString(offset === 0 ? this.str : this.str.slice(offset)) } /** * @return {number} */ getRef () { return 4 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentString} */ export const readContentString = decoder => new ContentString(decoder.readString()) yjs-13.6.8/src/structs/ContentType.js000066400000000000000000000067141450200430400175260ustar00rootroot00000000000000 import { readYArray, readYMap, readYText, readYXmlElement, readYXmlFragment, readYXmlHook, readYXmlText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' /** * @type {Array>} * @private */ export const typeRefs = [ readYArray, readYMap, readYText, readYXmlElement, readYXmlFragment, readYXmlHook, readYXmlText ] export const YArrayRefID = 0 export const YMapRefID = 1 export const YTextRefID = 2 export const YXmlElementRefID = 3 export const YXmlFragmentRefID = 4 export const YXmlHookRefID = 5 export const YXmlTextRefID = 6 /** * @private */ export class ContentType { /** * @param {AbstractType} type */ constructor (type) { /** * @type {AbstractType} */ this.type = type } /** * @return {number} */ getLength () { return 1 } /** * @return {Array} */ getContent () { return [this.type] } /** * @return {boolean} */ isCountable () { return true } /** * @return {ContentType} */ copy () { return new ContentType(this.type._copy()) } /** * @param {number} offset * @return {ContentType} */ splice (offset) { throw error.methodUnimplemented() } /** * @param {ContentType} right * @return {boolean} */ mergeWith (right) { return false } /** * @param {Transaction} transaction * @param {Item} item */ integrate (transaction, item) { this.type._integrate(transaction.doc, item) } /** * @param {Transaction} transaction */ delete (transaction) { let item = this.type._start while (item !== null) { if (!item.deleted) { item.delete(transaction) } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { // This will be gc'd later and we want to merge it if possible // We try to merge all deleted items after each transaction, // but we have no knowledge about that this needs to be merged // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs transaction._mergeStructs.push(item) } item = item.right } this.type._map.forEach(item => { if (!item.deleted) { item.delete(transaction) } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { // same as above transaction._mergeStructs.push(item) } }) transaction.changed.delete(this.type) } /** * @param {StructStore} store */ gc (store) { let item = this.type._start while (item !== null) { item.gc(store, true) item = item.right } this.type._start = null this.type._map.forEach(/** @param {Item | null} item */ (item) => { while (item !== null) { item.gc(store, true) item = item.left } }) this.type._map = new Map() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { this.type._write(encoder) } /** * @return {number} */ getRef () { return 7 } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentType} */ export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder)) yjs-13.6.8/src/structs/GC.js000066400000000000000000000022341450200430400155340ustar00rootroot00000000000000 import { AbstractStruct, addStruct, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line } from '../internals.js' export const structGCRefNumber = 0 /** * @private */ export class GC extends AbstractStruct { get deleted () { return true } delete () {} /** * @param {GC} right * @return {boolean} */ mergeWith (right) { if (this.constructor !== right.constructor) { return false } this.length += right.length return true } /** * @param {Transaction} transaction * @param {number} offset */ integrate (transaction, offset) { if (offset > 0) { this.id.clock += offset this.length -= offset } addStruct(transaction.doc.store, this) } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeInfo(structGCRefNumber) encoder.writeLen(this.length - offset) } /** * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing (transaction, store) { return null } } yjs-13.6.8/src/structs/Item.js000066400000000000000000000570771450200430400161600ustar00rootroot00000000000000 import { GC, getState, AbstractStruct, replaceStruct, addStruct, addToDeleteSet, findRootTypeKey, compareIDs, getItem, getItemCleanEnd, getItemCleanStart, readContentDeleted, readContentBinary, readContentJSON, readContentAny, readContentString, readContentEmbed, readContentDoc, createID, readContentFormat, readContentType, addChangedTypeToTransaction, isDeleted, StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' import * as binary from 'lib0/binary' import * as array from 'lib0/array' /** * @todo This should return several items * * @param {StructStore} store * @param {ID} id * @return {{item:Item, diff:number}} */ export const followRedone = (store, id) => { /** * @type {ID|null} */ let nextID = id let diff = 0 let item do { if (diff > 0) { nextID = createID(nextID.client, nextID.clock + diff) } item = getItem(store, nextID) diff = nextID.clock - item.id.clock nextID = item.redone } while (nextID !== null && item instanceof Item) return { item, diff } } /** * Make sure that neither item nor any of its parents is ever deleted. * * This property does not persist when storing it into a database or when * sending it to other peers * * @param {Item|null} item * @param {boolean} keep */ export const keepItem = (item, keep) => { while (item !== null && item.keep !== keep) { item.keep = keep item = /** @type {AbstractType} */ (item.parent)._item } } /** * Split leftItem into two items * @param {Transaction} transaction * @param {Item} leftItem * @param {number} diff * @return {Item} * * @function * @private */ export const splitItem = (transaction, leftItem, diff) => { // create rightItem const { client, clock } = leftItem.id const rightItem = new Item( createID(client, clock + diff), leftItem, createID(client, clock + diff - 1), leftItem.right, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, leftItem.content.splice(diff) ) if (leftItem.deleted) { rightItem.markDeleted() } if (leftItem.keep) { rightItem.keep = true } if (leftItem.redone !== null) { rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff) } // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) leftItem.right = rightItem // update right if (rightItem.right !== null) { rightItem.right.left = rightItem } // right is more specific. transaction._mergeStructs.push(rightItem) // update parent._map if (rightItem.parentSub !== null && rightItem.right === null) { /** @type {AbstractType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) } leftItem.length = diff return rightItem } /** * @param {Array} stack * @param {ID} id */ const isDeletedByUndoStack = (stack, id) => array.some(stack, /** @param {StackItem} s */ s => isDeleted(s.deletions, id)) /** * Redoes the effect of this operation. * * @param {Transaction} transaction The Yjs instance. * @param {Item} item * @param {Set} redoitems * @param {DeleteSet} itemsToDelete * @param {boolean} ignoreRemoteMapChanges * @param {import('../utils/UndoManager.js').UndoManager} um * * @return {Item|null} * * @private */ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => { const doc = transaction.doc const store = doc.store const ownClientID = doc.clientID const redone = item.redone if (redone !== null) { return getItemCleanStart(transaction, redone) } let parentItem = /** @type {AbstractType} */ (item.parent)._item /** * @type {Item|null} */ let left = null /** * @type {Item|null} */ let right // make sure that parent is redone if (parentItem !== null && parentItem.deleted === true) { // try to undo parent if it will be undone anyway if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) { return null } while (parentItem.redone !== null) { parentItem = getItemCleanStart(transaction, parentItem.redone) } } const parentType = parentItem === null ? /** @type {AbstractType} */ (item.parent) : /** @type {ContentType} */ (parentItem.content).type if (item.parentSub === null) { // Is an array item. Insert at the old position left = item.left right = item // find next cloned_redo items while (left !== null) { /** * @type {Item|null} */ let leftTrace = left // trace redone until parent matches while (leftTrace !== null && /** @type {AbstractType} */ (leftTrace.parent)._item !== parentItem) { leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone) } if (leftTrace !== null && /** @type {AbstractType} */ (leftTrace.parent)._item === parentItem) { left = leftTrace break } left = left.left } while (right !== null) { /** * @type {Item|null} */ let rightTrace = right // trace redone until parent matches while (rightTrace !== null && /** @type {AbstractType} */ (rightTrace.parent)._item !== parentItem) { rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone) } if (rightTrace !== null && /** @type {AbstractType} */ (rightTrace.parent)._item === parentItem) { right = rightTrace break } right = right.right } } else { right = null if (item.right && !ignoreRemoteMapChanges) { left = item // Iterate right while right is in itemsToDelete // If it is intended to delete right while item is redone, we can expect that item should replace right. while (left !== null && left.right !== null && (left.right.redone || isDeleted(itemsToDelete, left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id))) { left = left.right // follow redone while (left.redone) left = getItemCleanStart(transaction, left.redone) } if (left && left.right !== null) { // It is not possible to redo this item because it conflicts with a // change from another client return null } } else { left = parentType._map.get(item.parentSub) || null } } const nextClock = getState(store, ownClientID) const nextId = createID(ownClientID, nextClock) const redoneItem = new Item( nextId, left, left && left.lastId, right, right && right.id, parentType, item.parentSub, item.content.copy() ) item.redone = nextId keepItem(redoneItem, true) redoneItem.integrate(transaction, 0) return redoneItem } /** * Abstract class that represents any content. */ export class Item extends AbstractStruct { /** * @param {ID} id * @param {Item | null} left * @param {ID | null} origin * @param {Item | null} right * @param {ID | null} rightOrigin * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. * @param {string | null} parentSub * @param {AbstractContent} content */ constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) { super(id, content.getLength()) /** * The item that was originally to the left of this item. * @type {ID | null} */ this.origin = origin /** * The item that is currently to the left of this item. * @type {Item | null} */ this.left = left /** * The item that is currently to the right of this item. * @type {Item | null} */ this.right = right /** * The item that was originally to the right of this item. * @type {ID | null} */ this.rightOrigin = rightOrigin /** * @type {AbstractType|ID|null} */ this.parent = parent /** * If the parent refers to this item with some kind of key (e.g. YMap, the * key is specified here. The key is then used to refer to the list in which * to insert this item. If `parentSub = null` type._start is the list in * which to insert to. Otherwise it is `parent._map`. * @type {String | null} */ this.parentSub = parentSub /** * If this type's effect is redone this type refers to the type that undid * this operation. * @type {ID | null} */ this.redone = null /** * @type {AbstractContent} */ this.content = content /** * bit1: keep * bit2: countable * bit3: deleted * bit4: mark - mark node as fast-search-marker * @type {number} byte */ this.info = this.content.isCountable() ? binary.BIT2 : 0 } /** * This is used to mark the item as an indexed fast-search marker * * @type {boolean} */ set marker (isMarked) { if (((this.info & binary.BIT4) > 0) !== isMarked) { this.info ^= binary.BIT4 } } get marker () { return (this.info & binary.BIT4) > 0 } /** * If true, do not garbage collect this Item. */ get keep () { return (this.info & binary.BIT1) > 0 } set keep (doKeep) { if (this.keep !== doKeep) { this.info ^= binary.BIT1 } } get countable () { return (this.info & binary.BIT2) > 0 } /** * Whether this item was deleted or not. * @type {Boolean} */ get deleted () { return (this.info & binary.BIT3) > 0 } set deleted (doDelete) { if (this.deleted !== doDelete) { this.info ^= binary.BIT3 } } markDeleted () { this.info |= binary.BIT3 } /** * Return the creator clientID of the missing op or define missing items and return null. * * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing (transaction, store) { if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) { return this.origin.client } if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) { return this.rightOrigin.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 } // We have all missing ids, now find the items if (this.origin) { this.left = getItemCleanEnd(transaction, store, this.origin) this.origin = this.left.lastId } if (this.rightOrigin) { this.right = getItemCleanStart(transaction, this.rightOrigin) this.rightOrigin = this.right.id } if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) { this.parent = null } else if (!this.parent) { // only set parent if this shouldn't be garbage collected if (this.left && this.left.constructor === Item) { this.parent = this.left.parent this.parentSub = this.left.parentSub } if (this.right && this.right.constructor === Item) { this.parent = this.right.parent this.parentSub = this.right.parentSub } } else if (this.parent.constructor === ID) { const parentItem = getItem(store, this.parent) if (parentItem.constructor === GC) { this.parent = null } else { this.parent = /** @type {ContentType} */ (parentItem.content).type } } return null } /** * @param {Transaction} transaction * @param {number} offset */ integrate (transaction, offset) { if (offset > 0) { this.id.clock += offset this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)) this.origin = this.left.lastId this.content = this.content.splice(offset) this.length -= offset } if (this.parent) { if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) { /** * @type {Item|null} */ let left = this.left /** * @type {Item|null} */ let o // set o to the first conflicting item if (left !== null) { o = left.right } else if (this.parentSub !== null) { o = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null while (o !== null && o.left !== null) { o = o.left } } else { o = /** @type {AbstractType} */ (this.parent)._start } // TODO: use something like DeleteSet here (a tree implementation would be best) // @todo use global set definitions /** * @type {Set} */ const conflictingItems = new Set() /** * @type {Set} */ const itemsBeforeOrigin = new Set() // Let c in conflictingItems, b in itemsBeforeOrigin // ***{origin}bbbb{this}{c,b}{c,b}{o}*** // Note that conflictingItems is a subset of itemsBeforeOrigin while (o !== null && o !== this.right) { itemsBeforeOrigin.add(o) conflictingItems.add(o) if (compareIDs(this.origin, o.origin)) { // case 1 if (o.id.client < this.id.client) { left = o conflictingItems.clear() } else if (compareIDs(this.rightOrigin, o.rightOrigin)) { // this and o are conflicting and point to the same integration points. The id decides which item comes first. // Since this is to the left of o, we can break here break } // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items. // case 2 if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { left = o conflictingItems.clear() } } else { break } o = o.right } this.left = left } // reconnect left/right + update parent map/start if necessary if (this.left !== null) { const right = this.left.right this.right = right this.left.right = this } else { let r if (this.parentSub !== null) { r = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null while (r !== null && r.left !== null) { r = r.left } } else { r = /** @type {AbstractType} */ (this.parent)._start ;/** @type {AbstractType} */ (this.parent)._start = this } this.right = r } if (this.right !== null) { this.right.left = this } else if (this.parentSub !== null) { // set as current parent value if right === null and this is parentSub /** @type {AbstractType} */ (this.parent)._map.set(this.parentSub, this) if (this.left !== null) { // this is the current attribute value of parent. delete right this.left.delete(transaction) } } // adjust length of parent if (this.parentSub === null && this.countable && !this.deleted) { /** @type {AbstractType} */ (this.parent)._length += this.length } addStruct(transaction.doc.store, this) this.content.integrate(transaction, this) // add parent to transaction.changed addChangedTypeToTransaction(transaction, /** @type {AbstractType} */ (this.parent), this.parentSub) if ((/** @type {AbstractType} */ (this.parent)._item !== null && /** @type {AbstractType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent this.delete(transaction) } } else { // parent is not defined. Integrate GC struct instead new GC(this.id, this.length).integrate(transaction, 0) } } /** * Returns the next non-deleted item */ get next () { let n = this.right while (n !== null && n.deleted) { n = n.right } return n } /** * Returns the previous non-deleted item */ get prev () { let n = this.left while (n !== null && n.deleted) { n = n.left } return n } /** * Computes the last content address of this Item. */ get lastId () { // allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1) } /** * Try to merge two items * * @param {Item} right * @return {boolean} */ mergeWith (right) { if ( this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { const searchMarker = /** @type {AbstractType} */ (this.parent)._searchMarker if (searchMarker) { searchMarker.forEach(marker => { if (marker.p === right) { // right is going to be "forgotten" so we need to update the marker marker.p = this // adjust marker index if (!this.deleted && this.countable) { marker.index -= this.length } } }) } if (right.keep) { this.keep = true } this.right = right.right if (this.right !== null) { this.right.left = this } this.length += right.length return true } return false } /** * Mark this Item as deleted. * * @param {Transaction} transaction */ delete (transaction) { if (!this.deleted) { const parent = /** @type {AbstractType} */ (this.parent) // adjust the length of parent if (this.countable && this.parentSub === null) { parent._length -= this.length } this.markDeleted() addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length) addChangedTypeToTransaction(transaction, parent, this.parentSub) this.content.delete(transaction) } } /** * @param {StructStore} store * @param {boolean} parentGCd */ gc (store, parentGCd) { if (!this.deleted) { throw error.unexpectedCase() } this.content.gc(store) if (parentGCd) { replaceStruct(store, this, new GC(this.id, this.length)) } else { this.content = new ContentDeleted(this.length) } } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. * @param {number} offset */ write (encoder, offset) { const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin const rightOrigin = this.rightOrigin const parentSub = this.parentSub const info = (this.content.getRef() & binary.BITS5) | (origin === null ? 0 : binary.BIT8) | // origin is defined (rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined (parentSub === null ? 0 : binary.BIT6) // parentSub is non-null encoder.writeInfo(info) if (origin !== null) { encoder.writeLeftID(origin) } if (rightOrigin !== null) { encoder.writeRightID(rightOrigin) } if (origin === null && rightOrigin === null) { const parent = /** @type {AbstractType} */ (this.parent) if (parent._item !== undefined) { const parentItem = parent._item if (parentItem === null) { // parent type on y._map // find the correct key const ykey = findRootTypeKey(parent) encoder.writeParentInfo(true) // write parentYKey encoder.writeString(ykey) } else { encoder.writeParentInfo(false) // write parent id encoder.writeLeftID(parentItem.id) } } else if (parent.constructor === String) { // this edge case was added by differential updates encoder.writeParentInfo(true) // write parentYKey encoder.writeString(parent) } else if (parent.constructor === ID) { encoder.writeParentInfo(false) // write parent id encoder.writeLeftID(parent) } else { error.unexpectedCase() } if (parentSub !== null) { encoder.writeString(parentSub) } } this.content.write(encoder, offset) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {number} info */ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder) /** * A lookup map for reading Item content. * * @type {Array} */ export const contentRefs = [ () => { error.unexpectedCase() }, // GC is not ItemContent readContentDeleted, // 1 readContentJSON, // 2 readContentBinary, // 3 readContentString, // 4 readContentEmbed, // 5 readContentFormat, // 6 readContentType, // 7 readContentAny, // 8 readContentDoc, // 9 () => { error.unexpectedCase() } // 10 - Skip is not ItemContent ] /** * Do not implement this class! */ export class AbstractContent { /** * @return {number} */ getLength () { throw error.methodUnimplemented() } /** * @return {Array} */ getContent () { throw error.methodUnimplemented() } /** * Should return false if this Item is some kind of meta information * (e.g. format information). * * * Whether this Item should be addressable via `yarray.get(i)` * * Whether this Item should be counted when computing yarray.length * * @return {boolean} */ isCountable () { throw error.methodUnimplemented() } /** * @return {AbstractContent} */ copy () { throw error.methodUnimplemented() } /** * @param {number} _offset * @return {AbstractContent} */ splice (_offset) { throw error.methodUnimplemented() } /** * @param {AbstractContent} _right * @return {boolean} */ mergeWith (_right) { throw error.methodUnimplemented() } /** * @param {Transaction} _transaction * @param {Item} _item */ integrate (_transaction, _item) { throw error.methodUnimplemented() } /** * @param {Transaction} _transaction */ delete (_transaction) { throw error.methodUnimplemented() } /** * @param {StructStore} _store */ gc (_store) { throw error.methodUnimplemented() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder * @param {number} _offset */ write (_encoder, _offset) { throw error.methodUnimplemented() } /** * @return {number} */ getRef () { throw error.methodUnimplemented() } } yjs-13.6.8/src/structs/Skip.js000066400000000000000000000023651450200430400161560ustar00rootroot00000000000000 import { AbstractStruct, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' import * as encoding from 'lib0/encoding' export const structSkipRefNumber = 10 /** * @private */ export class Skip extends AbstractStruct { get deleted () { return true } delete () {} /** * @param {Skip} right * @return {boolean} */ mergeWith (right) { if (this.constructor !== right.constructor) { return false } this.length += right.length return true } /** * @param {Transaction} transaction * @param {number} offset */ integrate (transaction, offset) { // skip structs cannot be integrated error.unexpectedCase() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write (encoder, offset) { encoder.writeInfo(structSkipRefNumber) // write as VarUint because Skips can't make use of predictable length-encoding encoding.writeVarUint(encoder.restEncoder, this.length - offset) } /** * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing (transaction, store) { return null } } yjs-13.6.8/src/types/000077500000000000000000000000001450200430400143415ustar00rootroot00000000000000yjs-13.6.8/src/types/AbstractType.js000066400000000000000000000574541450200430400173230ustar00rootroot00000000000000 import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler, getState, isVisible, ContentType, createID, ContentAny, ContentBinary, getItemCleanStart, ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map' import * as iterator from 'lib0/iterator' import * as error from 'lib0/error' import * as math from 'lib0/math' const maxSearchMarker = 80 /** * A unique timestamp that identifies each marker. * * Time is relative,.. this is more like an ever-increasing clock. * * @type {number} */ let globalSearchMarkerTimestamp = 0 export class ArraySearchMarker { /** * @param {Item} p * @param {number} index */ constructor (p, index) { p.marker = true this.p = p this.index = index this.timestamp = globalSearchMarkerTimestamp++ } } /** * @param {ArraySearchMarker} marker */ const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ } /** * This is rather complex so this function is the only thing that should overwrite a marker * * @param {ArraySearchMarker} marker * @param {Item} p * @param {number} index */ const overwriteMarker = (marker, p, index) => { marker.p.marker = false marker.p = p p.marker = true marker.index = index marker.timestamp = globalSearchMarkerTimestamp++ } /** * @param {Array} searchMarker * @param {Item} p * @param {number} index */ const markPosition = (searchMarker, p, index) => { if (searchMarker.length >= maxSearchMarker) { // override oldest marker (we don't want to create more objects) const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b) overwriteMarker(marker, p, index) return marker } else { // create new marker const pm = new ArraySearchMarker(p, index) searchMarker.push(pm) return pm } } /** * Search marker help us to find positions in the associative array faster. * * They speed up the process of finding a position without much bookkeeping. * * A maximum of `maxSearchMarker` objects are created. * * This function always returns a refreshed marker (updated timestamp) * * @param {AbstractType} yarray * @param {number} index */ export const findMarker = (yarray, index) => { if (yarray._start === null || index === 0 || yarray._searchMarker === null) { return null } const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b) let p = yarray._start let pindex = 0 if (marker !== null) { p = marker.p pindex = marker.index refreshMarkerTimestamp(marker) // we used it, we might need to use it again } // iterate to right if possible while (p.right !== null && pindex < index) { if (!p.deleted && p.countable) { if (index < pindex + p.length) { break } pindex += p.length } p = p.right } // iterate to left if necessary (might be that pindex > index) while (p.left !== null && pindex > index) { p = p.left if (!p.deleted && p.countable) { pindex -= p.length } } // we want to make sure that p can't be merged with left, because that would screw up everything // in that cas just return what we have (it is most likely the best marker anyway) // iterate to left until p can't be merged with left while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { p = p.left if (!p.deleted && p.countable) { pindex -= p.length } } // @todo remove! // assure position // { // let start = yarray._start // let pos = 0 // while (start !== p) { // if (!start.deleted && start.countable) { // pos += start.length // } // start = /** @type {Item} */ (start.right) // } // if (pos !== pindex) { // debugger // throw new Error('Gotcha position fail!') // } // } // if (marker) { // if (window.lengthes == null) { // window.lengthes = [] // window.getLengthes = () => window.lengthes.sort((a, b) => a - b) // } // window.lengthes.push(marker.index - pindex) // console.log('distance', marker.index - pindex, 'len', p && p.parent.length) // } if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray} */ (p.parent).length / maxSearchMarker) { // adjust existing marker overwriteMarker(marker, p, pindex) return marker } else { // create new marker return markPosition(yarray._searchMarker, p, pindex) } } /** * Update markers when a change happened. * * This should be called before doing a deletion! * * @param {Array} searchMarker * @param {number} index * @param {number} len If insertion, len is positive. If deletion, len is negative. */ export const updateMarkerChanges = (searchMarker, index, len) => { for (let i = searchMarker.length - 1; i >= 0; i--) { const m = searchMarker[i] if (len > 0) { /** * @type {Item|null} */ let p = m.p p.marker = false // Ideally we just want to do a simple position comparison, but this will only work if // search markers don't point to deleted items for formats. // Iterate marker to prev undeleted countable position so we know what to do when updating a position while (p && (p.deleted || !p.countable)) { p = p.left if (p && !p.deleted && p.countable) { // adjust position. the loop should break now m.index -= p.length } } if (p === null || p.marker === true) { // remove search marker if updated position is null or if position is already marked searchMarker.splice(i, 1) continue } m.p = p p.marker = true } if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice m.index = math.max(index, m.index + len) } } } /** * Accumulate all (list) children of a type and return them as an Array. * * @param {AbstractType} t * @return {Array} */ export const getTypeChildren = t => { let s = t._start const arr = [] while (s) { arr.push(s) s = s.right } return arr } /** * Call event listeners with an event. This will also add an event to all * parents (for `.observeDeep` handlers). * * @template EventType * @param {AbstractType} type * @param {Transaction} transaction * @param {EventType} event */ export const callTypeObservers = (type, transaction, event) => { const changedType = type const changedParentTypes = transaction.changedParentTypes while (true) { // @ts-ignore map.setIfUndefined(changedParentTypes, type, () => []).push(event) if (type._item === null) { break } type = /** @type {AbstractType} */ (type._item.parent) } callEventHandlerListeners(changedType._eH, event, transaction) } /** * @template EventType * Abstract Yjs Type class */ export class AbstractType { constructor () { /** * @type {Item|null} */ this._item = null /** * @type {Map} */ this._map = new Map() /** * @type {Item|null} */ this._start = null /** * @type {Doc|null} */ this.doc = null this._length = 0 /** * Event handlers * @type {EventHandler} */ this._eH = createEventHandler() /** * Deep event handlers * @type {EventHandler>,Transaction>} */ this._dEH = createEventHandler() /** * @type {null | Array} */ this._searchMarker = null } /** * @return {AbstractType|null} */ get parent () { return this._item ? /** @type {AbstractType} */ (this._item.parent) : null } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item|null} item */ _integrate (y, item) { this.doc = y this._item = item } /** * @return {AbstractType} */ _copy () { throw error.methodUnimplemented() } /** * @return {AbstractType} */ clone () { throw error.methodUnimplemented() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder */ _write (_encoder) { } /** * The first non-deleted item */ get _first () { let n = this._start while (n !== null && n.deleted) { n = n.right } return n } /** * Creates YEvent and calls all type observers. * Must be implemented by each type. * * @param {Transaction} transaction * @param {Set} _parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, _parentSubs) { if (!transaction.local && this._searchMarker) { this._searchMarker.length = 0 } } /** * Observe all events that are created on this type. * * @param {function(EventType, Transaction):void} f Observer function */ observe (f) { addEventHandlerListener(this._eH, f) } /** * Observe all events that are created by this type and its children. * * @param {function(Array>,Transaction):void} f Observer function */ observeDeep (f) { addEventHandlerListener(this._dEH, f) } /** * Unregister an observer function. * * @param {function(EventType,Transaction):void} f Observer function */ unobserve (f) { removeEventHandlerListener(this._eH, f) } /** * Unregister an observer function. * * @param {function(Array>,Transaction):void} f Observer function */ unobserveDeep (f) { removeEventHandlerListener(this._dEH, f) } /** * @abstract * @return {any} */ toJSON () {} } /** * @param {AbstractType} type * @param {number} start * @param {number} end * @return {Array} * * @private * @function */ export const typeListSlice = (type, start, end) => { if (start < 0) { start = type._length + start } if (end < 0) { end = type._length + end } let len = end - start const cs = [] let n = type._start while (n !== null && len > 0) { if (n.countable && !n.deleted) { const c = n.content.getContent() if (c.length <= start) { start -= c.length } else { for (let i = start; i < c.length && len > 0; i++) { cs.push(c[i]) len-- } start = 0 } } n = n.right } return cs } /** * @param {AbstractType} type * @return {Array} * * @private * @function */ export const typeListToArray = type => { const cs = [] let n = type._start while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { cs.push(c[i]) } } n = n.right } return cs } /** * @param {AbstractType} type * @param {Snapshot} snapshot * @return {Array} * * @private * @function */ export const typeListToArraySnapshot = (type, snapshot) => { const cs = [] let n = type._start while (n !== null) { if (n.countable && isVisible(n, snapshot)) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { cs.push(c[i]) } } n = n.right } return cs } /** * Executes a provided function on once on overy element of this YArray. * * @param {AbstractType} type * @param {function(any,number,any):void} f A function to execute on every element of this YArray. * * @private * @function */ export const typeListForEach = (type, f) => { let index = 0 let n = type._start while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { f(c[i], index++, type) } } n = n.right } } /** * @template C,R * @param {AbstractType} type * @param {function(C,number,AbstractType):R} f * @return {Array} * * @private * @function */ export const typeListMap = (type, f) => { /** * @type {Array} */ const result = [] typeListForEach(type, (c, i) => { result.push(f(c, i, type)) }) return result } /** * @param {AbstractType} type * @return {IterableIterator} * * @private * @function */ export const typeListCreateIterator = type => { let n = type._start /** * @type {Array|null} */ let currentContent = null let currentContentIndex = 0 return { [Symbol.iterator] () { return this }, next: () => { // find some content if (currentContent === null) { while (n !== null && n.deleted) { n = n.right } // check if we reached the end, no need to check currentContent, because it does not exist if (n === null) { return { done: true, value: undefined } } // we found n, so we can set currentContent currentContent = n.content.getContent() currentContentIndex = 0 n = n.right // we used the content of n, now iterate to next } const value = currentContent[currentContentIndex++] // check if we need to empty currentContent if (currentContent.length <= currentContentIndex) { currentContent = null } return { done: false, value } } } } /** * Executes a provided function on once on overy element of this YArray. * Operates on a snapshotted state of the document. * * @param {AbstractType} type * @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray. * @param {Snapshot} snapshot * * @private * @function */ export const typeListForEachSnapshot = (type, f, snapshot) => { let index = 0 let n = type._start while (n !== null) { if (n.countable && isVisible(n, snapshot)) { const c = n.content.getContent() for (let i = 0; i < c.length; i++) { f(c[i], index++, type) } } n = n.right } } /** * @param {AbstractType} type * @param {number} index * @return {any} * * @private * @function */ export const typeListGet = (type, index) => { const marker = findMarker(type, index) let n = type._start if (marker !== null) { n = marker.p index -= marker.index } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { return n.content.getContent()[index] } index -= n.length } } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {Item?} referenceItem * @param {Array|Array|boolean|number|null|string|Uint8Array>} content * * @private * @function */ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { let left = referenceItem const doc = transaction.doc const ownClientId = doc.clientID const store = doc.store const right = referenceItem === null ? parent._start : referenceItem.right /** * @type {Array|number|null>} */ let jsonContent = [] const packJsonContent = () => { if (jsonContent.length > 0) { left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)) left.integrate(transaction, 0) jsonContent = [] } } content.forEach(c => { if (c === null) { jsonContent.push(c) } else { switch (c.constructor) { case Number: case Object: case Boolean: case Array: case String: jsonContent.push(c) break default: packJsonContent() switch (c.constructor) { case Uint8Array: case ArrayBuffer: left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) left.integrate(transaction, 0) break case Doc: left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c))) left.integrate(transaction, 0) break default: if (c instanceof AbstractType) { left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)) left.integrate(transaction, 0) } else { throw new Error('Unexpected content type in insert operation') } } } } }) packJsonContent() } const lengthExceeded = () => error.create('Length exceeded!') /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {Array|Array|number|null|string|Uint8Array>} content * * @private * @function */ export const typeListInsertGenerics = (transaction, parent, index, content) => { if (index > parent._length) { throw lengthExceeded() } if (index === 0) { if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, index, content.length) } return typeListInsertGenericsAfter(transaction, parent, null, content) } const startIndex = index const marker = findMarker(parent, index) let n = parent._start if (marker !== null) { n = marker.p index -= marker.index // we need to iterate one to the left so that the algorithm works if (index === 0) { // @todo refactor this as it actually doesn't consider formats n = n.prev // important! get the left undeleted item so that we can actually decrease index index += (n && n.countable && !n.deleted) ? n.length : 0 } } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index <= n.length) { if (index < n.length) { // insert in-between getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) } break } index -= n.length } } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, content.length) } return typeListInsertGenericsAfter(transaction, parent, n, content) } /** * Pushing content is special as we generally want to push after the last item. So we don't have to update * the serach marker. * * @param {Transaction} transaction * @param {AbstractType} parent * @param {Array|Array|number|null|string|Uint8Array>} content * * @private * @function */ export const typeListPushGenerics = (transaction, parent, content) => { // Use the marker with the highest index and iterate to the right. const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start }) let n = marker.p if (n) { while (n.right) { n = n.right } } return typeListInsertGenericsAfter(transaction, parent, n, content) } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {number} length * * @private * @function */ export const typeListDelete = (transaction, parent, index, length) => { if (length === 0) { return } const startIndex = index const startLength = length const marker = findMarker(parent, index) let n = parent._start if (marker !== null) { n = marker.p index -= marker.index } // compute the first item to be deleted for (; n !== null && index > 0; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) } index -= n.length } } // delete all items until done while (length > 0 && n !== null) { if (!n.deleted) { if (length < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length)) } n.delete(transaction) length -= n.length } n = n.right } if (length > 0) { throw lengthExceeded() } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */) } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * * @private * @function */ export const typeMapDelete = (transaction, parent, key) => { const c = parent._map.get(key) if (c !== undefined) { c.delete(transaction) } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * @param {Object|number|null|Array|string|Uint8Array|AbstractType} value * * @private * @function */ export const typeMapSet = (transaction, parent, key, value) => { const left = parent._map.get(key) || null const doc = transaction.doc const ownClientId = doc.clientID let content if (value == null) { content = new ContentAny([value]) } else { switch (value.constructor) { case Number: case Object: case Boolean: case Array: case String: content = new ContentAny([value]) break case Uint8Array: content = new ContentBinary(/** @type {Uint8Array} */ (value)) break case Doc: content = new ContentDoc(/** @type {Doc} */ (value)) break default: if (value instanceof AbstractType) { content = new ContentType(value) } else { throw new Error('Unexpected content type') } } } new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0) } /** * @param {AbstractType} parent * @param {string} key * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined} * * @private * @function */ export const typeMapGet = (parent, key) => { const val = parent._map.get(key) return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined } /** * @param {AbstractType} parent * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined>} * * @private * @function */ export const typeMapGetAll = (parent) => { /** * @type {Object} */ const res = {} parent._map.forEach((value, key) => { if (!value.deleted) { res[key] = value.content.getContent()[value.length - 1] } }) return res } /** * @param {AbstractType} parent * @param {string} key * @return {boolean} * * @private * @function */ export const typeMapHas = (parent, key) => { const val = parent._map.get(key) return val !== undefined && !val.deleted } /** * @param {AbstractType} parent * @param {string} key * @param {Snapshot} snapshot * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined} * * @private * @function */ export const typeMapGetSnapshot = (parent, key, snapshot) => { let v = parent._map.get(key) || null while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { v = v.left } return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined } /** * @param {Map} map * @return {IterableIterator>} * * @private * @function */ export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted) yjs-13.6.8/src/types/YArray.js000066400000000000000000000147051450200430400161150ustar00rootroot00000000000000/** * @module YArray */ import { YEvent, AbstractType, typeListGet, typeListToArray, typeListForEach, typeListCreateIterator, typeListInsertGenerics, typeListPushGenerics, typeListDelete, typeListMap, YArrayRefID, callTypeObservers, transact, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line } from '../internals.js' import { typeListSlice } from './AbstractType.js' /** * Event that describes the changes on a YArray * @template T * @extends YEvent> */ export class YArrayEvent extends YEvent { /** * @param {YArray} yarray The changed type * @param {Transaction} transaction The transaction object */ constructor (yarray, transaction) { super(yarray, transaction) this._transaction = transaction } } /** * A shared Array implementation. * @template T * @extends AbstractType> * @implements {Iterable} */ export class YArray extends AbstractType { constructor () { super() /** * @type {Array?} * @private */ this._prelimContent = [] /** * @type {Array} */ this._searchMarker = [] } /** * Construct a new YArray containing the specified items. * @template {Object|Array|number|null|string|Uint8Array} T * @param {Array} items * @return {YArray} */ static from (items) { /** * @type {YArray} */ const a = new YArray() a.push(items) return a } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate (y, item) { super._integrate(y, item) this.insert(0, /** @type {Array} */ (this._prelimContent)) this._prelimContent = null } /** * @return {YArray} */ _copy () { return new YArray() } /** * @return {YArray} */ clone () { /** * @type {YArray} */ const arr = new YArray() arr.insert(0, this.toArray().map(el => el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el )) return arr } get length () { return this._prelimContent === null ? this._length : this._prelimContent.length } /** * Creates YArrayEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { super._callObserver(transaction, parentSubs) callTypeObservers(this, transaction, new YArrayEvent(this, transaction)) } /** * Inserts new content at an index. * * Important: This function expects an array of content. Not just a content * object. The reason for this "weirdness" is that inserting several elements * is very efficient when it is done as a single operation. * * @example * // Insert character 'a' at position 0 * yarray.insert(0, ['a']) * // Insert numbers 1, 2 at position 1 * yarray.insert(1, [1, 2]) * * @param {number} index The index to insert content at. * @param {Array} content The array of content */ insert (index, content) { if (this.doc !== null) { transact(this.doc, transaction => { typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content)) }) } else { /** @type {Array} */ (this._prelimContent).splice(index, 0, ...content) } } /** * Appends content to this YArray. * * @param {Array} content Array of content to append. * * @todo Use the following implementation in all types. */ push (content) { if (this.doc !== null) { transact(this.doc, transaction => { typeListPushGenerics(transaction, this, /** @type {any} */ (content)) }) } else { /** @type {Array} */ (this._prelimContent).push(...content) } } /** * Preppends content to this YArray. * * @param {Array} content Array of content to preppend. */ unshift (content) { this.insert(0, content) } /** * Deletes elements starting from an index. * * @param {number} index Index at which to start deleting elements * @param {number} length The number of elements to remove. Defaults to 1. */ delete (index, length = 1) { if (this.doc !== null) { transact(this.doc, transaction => { typeListDelete(transaction, this, index, length) }) } else { /** @type {Array} */ (this._prelimContent).splice(index, length) } } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray * @return {T} */ get (index) { return typeListGet(this, index) } /** * Transforms this YArray to a JavaScript Array. * * @return {Array} */ toArray () { return typeListToArray(this) } /** * Transforms this YArray to a JavaScript Array. * * @param {number} [start] * @param {number} [end] * @return {Array} */ slice (start = 0, end = this.length) { return typeListSlice(this, start, end) } /** * Transforms this Shared Type to a JSON object. * * @return {Array} */ toJSON () { return this.map(c => c instanceof AbstractType ? c.toJSON() : c) } /** * Returns an Array with the result of calling a provided function on every * element of this YArray. * * @template M * @param {function(T,number,YArray):M} f Function that produces an element of the new Array * @return {Array} A new array with each element being the result of the * callback function */ map (f) { return typeListMap(this, /** @type {any} */ (f)) } /** * Executes a provided function once on overy element of this YArray. * * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. */ forEach (f) { typeListForEach(this, f) } /** * @return {IterableIterator} */ [Symbol.iterator] () { return typeListCreateIterator(this) } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write (encoder) { encoder.writeTypeRef(YArrayRefID) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * * @private * @function */ export const readYArray = _decoder => new YArray() yjs-13.6.8/src/types/YMap.js000066400000000000000000000142301450200430400155450ustar00rootroot00000000000000 /** * @module YMap */ import { YEvent, AbstractType, typeMapDelete, typeMapSet, typeMapGet, typeMapHas, createMapIterator, YMapRefID, callTypeObservers, transact, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line } from '../internals.js' import * as iterator from 'lib0/iterator' /** * @template T * @extends YEvent> * Event that describes the changes on a YMap. */ export class YMapEvent extends YEvent { /** * @param {YMap} ymap The YArray that changed. * @param {Transaction} transaction * @param {Set} subs The keys that changed. */ constructor (ymap, transaction, subs) { super(ymap, transaction) this.keysChanged = subs } } /** * @template MapType * A shared Map implementation. * * @extends AbstractType> * @implements {Iterable} */ export class YMap extends AbstractType { /** * * @param {Iterable=} entries - an optional iterable to initialize the YMap */ constructor (entries) { super() /** * @type {Map?} * @private */ this._prelimContent = null if (entries === undefined) { this._prelimContent = new Map() } else { this._prelimContent = new Map(entries) } } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate (y, item) { super._integrate(y, item) ;/** @type {Map} */ (this._prelimContent).forEach((value, key) => { this.set(key, value) }) this._prelimContent = null } /** * @return {YMap} */ _copy () { return new YMap() } /** * @return {YMap} */ clone () { /** * @type {YMap} */ const map = new YMap() this.forEach((value, key) => { map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value) }) return map } /** * Creates YMapEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)) } /** * Transforms this Shared Type to a JSON object. * * @return {Object} */ toJSON () { /** * @type {Object} */ const map = {} this._map.forEach((item, key) => { if (!item.deleted) { const v = item.content.getContent()[item.length - 1] map[key] = v instanceof AbstractType ? v.toJSON() : v } }) return map } /** * Returns the size of the YMap (count of key/value pairs) * * @return {number} */ get size () { return [...createMapIterator(this._map)].length } /** * Returns the keys for each element in the YMap Type. * * @return {IterableIterator} */ keys () { return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0]) } /** * Returns the values for each element in the YMap Type. * * @return {IterableIterator} */ values () { return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1]) } /** * Returns an Iterator of [key, value] pairs * * @return {IterableIterator} */ entries () { return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]]) } /** * Executes a provided function on once on every key-value pair. * * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. */ forEach (f) { this._map.forEach((item, key) => { if (!item.deleted) { f(item.content.getContent()[item.length - 1], key, this) } }) } /** * Returns an Iterator of [key, value] pairs * * @return {IterableIterator} */ [Symbol.iterator] () { return this.entries() } /** * Remove a specified element from this YMap. * * @param {string} key The key of the element to remove. */ delete (key) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapDelete(transaction, this, key) }) } else { /** @type {Map} */ (this._prelimContent).delete(key) } } /** * Adds or updates an element with a specified key and value. * @template {MapType} VAL * * @param {string} key The key of the element to add to this YMap * @param {VAL} value The value of the element to add * @return {VAL} */ set (key, value) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapSet(transaction, this, key, /** @type {any} */ (value)) }) } else { /** @type {Map} */ (this._prelimContent).set(key, value) } return value } /** * Returns a specified element from this YMap. * * @param {string} key * @return {MapType|undefined} */ get (key) { return /** @type {any} */ (typeMapGet(this, key)) } /** * Returns a boolean indicating whether the specified key exists or not. * * @param {string} key The key to test. * @return {boolean} */ has (key) { return typeMapHas(this, key) } /** * Removes all elements from this YMap. */ clear () { if (this.doc !== null) { transact(this.doc, transaction => { this.forEach(function (_value, key, map) { typeMapDelete(transaction, map, key) }) }) } else { /** @type {Map} */ (this._prelimContent).clear() } } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write (encoder) { encoder.writeTypeRef(YMapRefID) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * * @private * @function */ export const readYMap = _decoder => new YMap() yjs-13.6.8/src/types/YText.js000066400000000000000000001172331450200430400157630ustar00rootroot00000000000000 /** * @module YText */ import { YEvent, AbstractType, getItemCleanStart, getState, isVisible, createID, YTextRefID, callTypeObservers, transact, ContentEmbed, GC, ContentFormat, ContentString, splitSnapshotAffectedStructs, iterateDeletedStructs, iterateStructs, findMarker, typeMapDelete, typeMapSet, typeMapGet, typeMapGetAll, updateMarkerChanges, ContentType, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line } from '../internals.js' import * as object from 'lib0/object' import * as map from 'lib0/map' import * as error from 'lib0/error' /** * @param {any} a * @param {any} b * @return {boolean} */ const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b)) export class ItemTextListPosition { /** * @param {Item|null} left * @param {Item|null} right * @param {number} index * @param {Map} currentAttributes */ constructor (left, right, index, currentAttributes) { this.left = left this.right = right this.index = index this.currentAttributes = currentAttributes } /** * Only call this if you know that this.right is defined */ forward () { if (this.right === null) { error.unexpectedCase() } switch (this.right.content.constructor) { case ContentFormat: if (!this.right.deleted) { updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content)) } break default: if (!this.right.deleted) { this.index += this.right.length } break } this.left = this.right this.right = this.right.right } } /** * @param {Transaction} transaction * @param {ItemTextListPosition} pos * @param {number} count steps to move forward * @return {ItemTextListPosition} * * @private * @function */ const findNextPosition = (transaction, pos, count) => { while (pos.right !== null && count > 0) { switch (pos.right.content.constructor) { case ContentFormat: if (!pos.right.deleted) { updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content)) } break default: if (!pos.right.deleted) { if (count < pos.right.length) { // split right getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)) } pos.index += pos.right.length count -= pos.right.length } break } pos.left = pos.right pos.right = pos.right.right // pos.forward() - we don't forward because that would halve the performance because we already do the checks above } return pos } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @return {ItemTextListPosition} * * @private * @function */ const findPosition = (transaction, parent, index) => { const currentAttributes = new Map() const marker = findMarker(parent, index) if (marker) { const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes) return findNextPosition(transaction, pos, index - marker.index) } else { const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes) return findNextPosition(transaction, pos, index) } } /** * Negate applied formats * * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {Map} negatedAttributes * * @private * @function */ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { // check if we really need to remove attributes while ( currPos.right !== null && ( currPos.right.deleted === true || ( currPos.right.content.constructor === ContentFormat && equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value) ) ) ) { if (!currPos.right.deleted) { negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key) } currPos.forward() } const doc = transaction.doc const ownClientId = doc.clientID negatedAttributes.forEach((val, key) => { const left = currPos.left const right = currPos.right const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) nextFormat.integrate(transaction, 0) currPos.right = nextFormat currPos.forward() }) } /** * @param {Map} currentAttributes * @param {ContentFormat} format * * @private * @function */ const updateCurrentAttributes = (currentAttributes, format) => { const { key, value } = format if (value === null) { currentAttributes.delete(key) } else { currentAttributes.set(key, value) } } /** * @param {ItemTextListPosition} currPos * @param {Object} attributes * * @private * @function */ const minimizeAttributeChanges = (currPos, attributes) => { // go right while attributes[right.key] === right.value (or right is deleted) while (true) { if (currPos.right === null) { break } else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) { // } else { break } currPos.forward() } } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {Object} attributes * @return {Map} * * @private * @function **/ const insertAttributes = (transaction, parent, currPos, attributes) => { const doc = transaction.doc const ownClientId = doc.clientID const negatedAttributes = new Map() // insert format-start items for (const key in attributes) { const val = attributes[key] const currentVal = currPos.currentAttributes.get(key) || null if (!equalAttrs(currentVal, val)) { // save negated attribute (set null if currentVal undefined) negatedAttributes.set(key, currentVal) const { left, right } = currPos currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) currPos.right.integrate(transaction, 0) currPos.forward() } } return negatedAttributes } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {string|object|AbstractType} text * @param {Object} attributes * * @private * @function **/ const insertText = (transaction, parent, currPos, text, attributes) => { currPos.currentAttributes.forEach((_val, key) => { if (attributes[key] === undefined) { attributes[key] = null } }) const doc = transaction.doc const ownClientId = doc.clientID minimizeAttributeChanges(currPos, attributes) const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes) // insert content const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text)) let { left, right, index } = currPos if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()) } right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content) right.integrate(transaction, 0) currPos.right = right currPos.index = index currPos.forward() insertNegatedAttributes(transaction, parent, currPos, negatedAttributes) } /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {number} length * @param {Object} attributes * * @private * @function */ const formatText = (transaction, parent, currPos, length, attributes) => { const doc = transaction.doc const ownClientId = doc.clientID minimizeAttributeChanges(currPos, attributes) const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes) // iterate until first non-format or null is found // delete all formats with attributes[format.key] != null // also check the attributes after the first non-format as we do not want to insert redundant negated attributes there // eslint-disable-next-line no-labels iterationLoop: while ( currPos.right !== null && (length > 0 || ( negatedAttributes.size > 0 && (currPos.right.deleted || currPos.right.content.constructor === ContentFormat) ) ) ) { if (!currPos.right.deleted) { switch (currPos.right.content.constructor) { case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (currPos.right.content) const attr = attributes[key] if (attr !== undefined) { if (equalAttrs(attr, value)) { negatedAttributes.delete(key) } else { if (length === 0) { // no need to further extend negatedAttributes // eslint-disable-next-line no-labels break iterationLoop } negatedAttributes.set(key, value) } currPos.right.delete(transaction) } else { currPos.currentAttributes.set(key, value) } break } default: if (length < currPos.right.length) { getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)) } length -= currPos.right.length break } } currPos.forward() } // Quill just assumes that the editor starts with a newline and that it always // ends with a newline. We only insert that newline when a new newline is // inserted - i.e when length is bigger than type.length if (length > 0) { let newlines = '' for (; length > 0; length--) { newlines += '\n' } currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines)) currPos.right.integrate(transaction, 0) currPos.forward() } insertNegatedAttributes(transaction, parent, currPos, negatedAttributes) } /** * Call this function after string content has been deleted in order to * clean up formatting Items. * * @param {Transaction} transaction * @param {Item} start * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item * @param {Map} startAttributes * @param {Map} currAttributes * @return {number} The amount of formatting Items deleted. * * @function */ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { /** * @type {Item|null} */ let end = start /** * @type {Map} */ const endFormats = map.create() while (end && (!end.countable || end.deleted)) { if (!end.deleted && end.content.constructor === ContentFormat) { const cf = /** @type {ContentFormat} */ (end.content) endFormats.set(cf.key, cf) } end = end.right } let cleanups = 0 let reachedCurr = false while (start !== end) { if (curr === start) { reachedCurr = true } if (!start.deleted) { const content = start.content switch (content.constructor) { case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (content) const startAttrValue = startAttributes.get(key) || null if (endFormats.get(key) !== content || startAttrValue === value) { // Either this format is overwritten or it is not necessary because the attribute already existed. start.delete(transaction) cleanups++ if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) { if (startAttrValue === null) { currAttributes.delete(key) } else { currAttributes.set(key, startAttrValue) } } } if (!reachedCurr && !start.deleted) { updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content)) } break } } } start = /** @type {Item} */ (start.right) } return cleanups } /** * @param {Transaction} transaction * @param {Item | null} item */ const cleanupContextlessFormattingGap = (transaction, item) => { // iterate until item.right is null or content while (item && item.right && (item.right.deleted || !item.right.countable)) { item = item.right } const attrs = new Set() // iterate back until a content item is found while (item && (item.deleted || !item.countable)) { if (!item.deleted && item.content.constructor === ContentFormat) { const key = /** @type {ContentFormat} */ (item.content).key if (attrs.has(key)) { item.delete(transaction) } else { attrs.add(key) } } item = item.left } } /** * This function is experimental and subject to change / be removed. * * Ideally, we don't need this function at all. Formatting attributes should be cleaned up * automatically after each change. This function iterates twice over the complete YText type * and removes unnecessary formatting attributes. This is also helpful for testing. * * This function won't be exported anymore as soon as there is confidence that the YText type works as intended. * * @param {YText} type * @return {number} How many formatting attributes have been cleaned up. */ export const cleanupYTextFormatting = type => { let res = 0 transact(/** @type {Doc} */ (type.doc), transaction => { let start = /** @type {Item} */ (type._start) let end = type._start let startAttributes = map.create() const currentAttributes = map.copy(startAttributes) while (end) { if (end.deleted === false) { switch (end.content.constructor) { case ContentFormat: updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content)) break default: res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes) startAttributes = map.copy(currentAttributes) start = end break } } end = end.right } }) return res } /** * This will be called by the transction once the event handlers are called to potentially cleanup * formatting attributes. * * @param {Transaction} transaction */ export const cleanupYTextAfterTransaction = transaction => { /** * @type {Set} */ const needFullCleanup = new Set() // check if another formatting item was inserted const doc = transaction.doc for (const [client, afterClock] of transaction.afterState.entries()) { const clock = transaction.beforeState.get(client) || 0 if (afterClock === clock) { continue } iterateStructs(transaction, /** @type {Array} */ (doc.store.clients.get(client)), clock, afterClock, item => { if ( !item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC ) { needFullCleanup.add(/** @type {any} */ (item).parent) } }) } // cleanup in a new transaction transact(doc, (t) => { iterateDeletedStructs(transaction, transaction.deleteSet, item => { if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) { return } const parent = /** @type {YText} */ (item.parent) if (item.content.constructor === ContentFormat) { needFullCleanup.add(parent) } else { // If no formatting attribute was inserted or deleted, we can make due with contextless // formatting cleanups. // Contextless: it is not necessary to compute currentAttributes for the affected position. cleanupContextlessFormattingGap(t, item) } }) // If a formatting item was inserted, we simply clean the whole type. // We need to compute currentAttributes for the current position anyway. for (const yText of needFullCleanup) { cleanupYTextFormatting(yText) } }) } /** * @param {Transaction} transaction * @param {ItemTextListPosition} currPos * @param {number} length * @return {ItemTextListPosition} * * @private * @function */ const deleteText = (transaction, currPos, length) => { const startLength = length const startAttrs = map.copy(currPos.currentAttributes) const start = currPos.right while (length > 0 && currPos.right !== null) { if (currPos.right.deleted === false) { switch (currPos.right.content.constructor) { case ContentType: case ContentEmbed: case ContentString: if (length < currPos.right.length) { getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)) } length -= currPos.right.length currPos.right.delete(transaction) break } } currPos.forward() } if (start) { cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes) } const parent = /** @type {AbstractType} */ (/** @type {Item} */ (currPos.left || currPos.right).parent) if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length) } return currPos } /** * The Quill Delta format represents changes on a text document with * formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta} * * @example * { * ops: [ * { insert: 'Gandalf', attributes: { bold: true } }, * { insert: ' the ' }, * { insert: 'Grey', attributes: { color: '#cccccc' } } * ] * } * */ /** * Attributes that can be assigned to a selection of text. * * @example * { * bold: true, * font-size: '40px' * } * * @typedef {Object} TextAttributes */ /** * @extends YEvent * Event that describes the changes on a YText type. */ export class YTextEvent extends YEvent { /** * @param {YText} ytext * @param {Transaction} transaction * @param {Set} subs The keys that changed */ constructor (ytext, transaction, subs) { super(ytext, transaction) /** * Whether the children changed. * @type {Boolean} * @private */ this.childListChanged = false /** * Set of all changed attributes. * @type {Set} */ this.keysChanged = new Set() subs.forEach((sub) => { if (sub === null) { this.childListChanged = true } else { this.keysChanged.add(sub) } }) } /** * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} */ get changes () { if (this._changes === null) { /** * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string|AbstractType|object, delete?:number, retain?:number}>}} */ const changes = { keys: this.keys, delta: this.delta, added: new Set(), deleted: new Set() } this._changes = changes } return /** @type {any} */ (this._changes) } /** * Compute the changes in the delta format. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} * * @public */ get delta () { if (this._delta === null) { const y = /** @type {Doc} */ (this.target.doc) /** * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} */ const delta = [] transact(y, transaction => { const currentAttributes = new Map() // saves all current attributes for insert const oldAttributes = new Map() let item = this.target._start /** * @type {string?} */ let action = null /** * @type {Object} */ const attributes = {} // counts added or removed new attributes for retain /** * @type {string|object} */ let insert = '' let retain = 0 let deleteLen = 0 const addOp = () => { if (action !== null) { /** * @type {any} */ let op = null switch (action) { case 'delete': if (deleteLen > 0) { op = { delete: deleteLen } } deleteLen = 0 break case 'insert': if (typeof insert === 'object' || insert.length > 0) { op = { insert } if (currentAttributes.size > 0) { op.attributes = {} currentAttributes.forEach((value, key) => { if (value !== null) { op.attributes[key] = value } }) } } insert = '' break case 'retain': if (retain > 0) { op = { retain } if (!object.isEmpty(attributes)) { op.attributes = object.assign({}, attributes) } } retain = 0 break } if (op) delta.push(op) action = null } } while (item !== null) { switch (item.content.constructor) { case ContentType: case ContentEmbed: if (this.adds(item)) { if (!this.deletes(item)) { addOp() action = 'insert' insert = item.content.getContent()[0] addOp() } } else if (this.deletes(item)) { if (action !== 'delete') { addOp() action = 'delete' } deleteLen += 1 } else if (!item.deleted) { if (action !== 'retain') { addOp() action = 'retain' } retain += 1 } break case ContentString: if (this.adds(item)) { if (!this.deletes(item)) { if (action !== 'insert') { addOp() action = 'insert' } insert += /** @type {ContentString} */ (item.content).str } } else if (this.deletes(item)) { if (action !== 'delete') { addOp() action = 'delete' } deleteLen += item.length } else if (!item.deleted) { if (action !== 'retain') { addOp() action = 'retain' } retain += item.length } break case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (item.content) if (this.adds(item)) { if (!this.deletes(item)) { const curVal = currentAttributes.get(key) || null if (!equalAttrs(curVal, value)) { if (action === 'retain') { addOp() } if (equalAttrs(value, (oldAttributes.get(key) || null))) { delete attributes[key] } else { attributes[key] = value } } else if (value !== null) { item.delete(transaction) } } } else if (this.deletes(item)) { oldAttributes.set(key, value) const curVal = currentAttributes.get(key) || null if (!equalAttrs(curVal, value)) { if (action === 'retain') { addOp() } attributes[key] = curVal } } else if (!item.deleted) { oldAttributes.set(key, value) const attr = attributes[key] if (attr !== undefined) { if (!equalAttrs(attr, value)) { if (action === 'retain') { addOp() } if (value === null) { delete attributes[key] } else { attributes[key] = value } } else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function item.delete(transaction) } } } if (!item.deleted) { if (action === 'insert') { addOp() } updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content)) } break } } item = item.right } addOp() while (delta.length > 0) { const lastOp = delta[delta.length - 1] if (lastOp.retain !== undefined && lastOp.attributes === undefined) { // retain delta's if they don't assign attributes delta.pop() } else { break } } }) this._delta = delta } return /** @type {any} */ (this._delta) } } /** * Type that represents text with formatting information. * * This type replaces y-richtext as this implementation is able to handle * block formats (format information on a paragraph), embeds (complex elements * like pictures and videos), and text formats (**bold**, *italic*). * * @extends AbstractType */ export class YText extends AbstractType { /** * @param {String} [string] The initial value of the YText. */ constructor (string) { super() /** * Array of pending operations on this type * @type {Array?} */ this._pending = string !== undefined ? [() => this.insert(0, string)] : [] /** * @type {Array|null} */ this._searchMarker = [] /** * Whether this YText contains formatting attributes. * This flag is updated when a formatting item is integrated (see ContentFormat.integrate) */ this._hasFormatting = false } /** * Number of characters of this text type. * * @type {number} */ get length () { return this._length } /** * @param {Doc} y * @param {Item} item */ _integrate (y, item) { super._integrate(y, item) try { /** @type {Array} */ (this._pending).forEach(f => f()) } catch (e) { console.error(e) } this._pending = null } _copy () { return new YText() } /** * @return {YText} */ clone () { const text = new YText() text.applyDelta(this.toDelta()) return text } /** * Creates YTextEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { super._callObserver(transaction, parentSubs) const event = new YTextEvent(this, transaction, parentSubs) callTypeObservers(this, transaction, event) // If a remote change happened, we try to cleanup potential formatting duplicates. if (!transaction.local && this._hasFormatting) { transaction._needFormattingCleanup = true } } /** * Returns the unformatted string representation of this YText type. * * @public */ toString () { let str = '' /** * @type {Item|null} */ let n = this._start while (n !== null) { if (!n.deleted && n.countable && n.content.constructor === ContentString) { str += /** @type {ContentString} */ (n.content).str } n = n.right } return str } /** * Returns the unformatted string representation of this YText type. * * @return {string} * @public */ toJSON () { return this.toString() } /** * Apply a {@link Delta} on this shared YText type. * * @param {any} delta The changes to apply on this element. * @param {object} opts * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. * * * @public */ applyDelta (delta, { sanitize = true } = {}) { if (this.doc !== null) { transact(this.doc, transaction => { const currPos = new ItemTextListPosition(null, this._start, 0, new Map()) for (let i = 0; i < delta.length; i++) { const op = delta[i] if (op.insert !== undefined) { // Quill assumes that the content starts with an empty paragraph. // Yjs/Y.Text assumes that it starts empty. We always hide that // there is a newline at the end of the content. // If we omit this step, clients will see a different number of // paragraphs, but nothing bad will happen. const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert if (typeof ins !== 'string' || ins.length > 0) { insertText(transaction, this, currPos, ins, op.attributes || {}) } } else if (op.retain !== undefined) { formatText(transaction, this, currPos, op.retain, op.attributes || {}) } else if (op.delete !== undefined) { deleteText(transaction, currPos, op.delete) } } }) } else { /** @type {Array} */ (this._pending).push(() => this.applyDelta(delta)) } } /** * Returns the Delta representation of this YText type. * * @param {Snapshot} [snapshot] * @param {Snapshot} [prevSnapshot] * @param {function('removed' | 'added', ID):any} [computeYChange] * @return {any} The Delta representation of this type. * * @public */ toDelta (snapshot, prevSnapshot, computeYChange) { /** * @type{Array} */ const ops = [] const currentAttributes = new Map() const doc = /** @type {Doc} */ (this.doc) let str = '' let n = this._start function packStr () { if (str.length > 0) { // pack str with attributes to ops /** * @type {Object} */ const attributes = {} let addAttributes = false currentAttributes.forEach((value, key) => { addAttributes = true attributes[key] = value }) /** * @type {Object} */ const op = { insert: str } if (addAttributes) { op.attributes = attributes } ops.push(op) str = '' } } const computeDelta = () => { while (n !== null) { if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) { switch (n.content.constructor) { case ContentString: { const cur = currentAttributes.get('ychange') if (snapshot !== undefined && !isVisible(n, snapshot)) { if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') { packStr() currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' }) } } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) { if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') { packStr() currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' }) } } else if (cur !== undefined) { packStr() currentAttributes.delete('ychange') } str += /** @type {ContentString} */ (n.content).str break } case ContentType: case ContentEmbed: { packStr() /** * @type {Object} */ const op = { insert: n.content.getContent()[0] } if (currentAttributes.size > 0) { const attrs = /** @type {Object} */ ({}) op.attributes = attrs currentAttributes.forEach((value, key) => { attrs[key] = value }) } ops.push(op) break } case ContentFormat: if (isVisible(n, snapshot)) { packStr() updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content)) } break } } n = n.right } packStr() } if (snapshot || prevSnapshot) { // snapshots are merged again after the transaction, so we need to keep the // transaction alive until we are done transact(doc, transaction => { if (snapshot) { splitSnapshotAffectedStructs(transaction, snapshot) } if (prevSnapshot) { splitSnapshotAffectedStructs(transaction, prevSnapshot) } computeDelta() }, 'cleanup') } else { computeDelta() } return ops } /** * Insert text at a given index. * * @param {number} index The index at which to start inserting. * @param {String} text The text to insert at the specified position. * @param {TextAttributes} [attributes] Optionally define some formatting * information to apply on the inserted * Text. * @public */ insert (index, text, attributes) { if (text.length <= 0) { return } const y = this.doc if (y !== null) { transact(y, transaction => { const pos = findPosition(transaction, this, index) if (!attributes) { attributes = {} // @ts-ignore pos.currentAttributes.forEach((v, k) => { attributes[k] = v }) } insertText(transaction, this, pos, text, attributes) }) } else { /** @type {Array} */ (this._pending).push(() => this.insert(index, text, attributes)) } } /** * Inserts an embed at a index. * * @param {number} index The index to insert the embed at. * @param {Object | AbstractType} embed The Object that represents the embed. * @param {TextAttributes} attributes Attribute information to apply on the * embed * * @public */ insertEmbed (index, embed, attributes = {}) { const y = this.doc if (y !== null) { transact(y, transaction => { const pos = findPosition(transaction, this, index) insertText(transaction, this, pos, embed, attributes) }) } else { /** @type {Array} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes)) } } /** * Deletes text starting from an index. * * @param {number} index Index at which to start deleting. * @param {number} length The number of characters to remove. Defaults to 1. * * @public */ delete (index, length) { if (length === 0) { return } const y = this.doc if (y !== null) { transact(y, transaction => { deleteText(transaction, findPosition(transaction, this, index), length) }) } else { /** @type {Array} */ (this._pending).push(() => this.delete(index, length)) } } /** * Assigns properties to a range of text. * * @param {number} index The position where to start formatting. * @param {number} length The amount of characters to assign properties to. * @param {TextAttributes} attributes Attribute information to apply on the * text. * * @public */ format (index, length, attributes) { if (length === 0) { return } const y = this.doc if (y !== null) { transact(y, transaction => { const pos = findPosition(transaction, this, index) if (pos.right === null) { return } formatText(transaction, this, pos, length, attributes) }) } else { /** @type {Array} */ (this._pending).push(() => this.format(index, length, attributes)) } } /** * Removes an attribute. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that is to be removed. * * @public */ removeAttribute (attributeName) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapDelete(transaction, this, attributeName) }) } else { /** @type {Array} */ (this._pending).push(() => this.removeAttribute(attributeName)) } } /** * Sets or updates an attribute. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that is to be set. * @param {any} attributeValue The attribute value that is to be set. * * @public */ setAttribute (attributeName, attributeValue) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapSet(transaction, this, attributeName, attributeValue) }) } else { /** @type {Array} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue)) } } /** * Returns an attribute value that belongs to the attribute name. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that identifies the * queried value. * @return {any} The queried attribute value. * * @public */ getAttribute (attributeName) { return /** @type {any} */ (typeMapGet(this, attributeName)) } /** * Returns all attribute name/value pairs in a JSON Object. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @return {Object} A JSON Object that describes the attributes. * * @public */ getAttributes () { return typeMapGetAll(this) } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write (encoder) { encoder.writeTypeRef(YTextRefID) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * @return {YText} * * @private * @function */ export const readYText = _decoder => new YText() yjs-13.6.8/src/types/YXmlElement.js000066400000000000000000000161571450200430400171140ustar00rootroot00000000000000import * as object from 'lib0/object' import { YXmlFragment, transact, typeMapDelete, typeMapHas, typeMapSet, typeMapGet, typeMapGetAll, typeListForEach, YXmlElementRefID, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line } from '../internals.js' /** * @typedef {Object|number|null|Array|string|Uint8Array|AbstractType} ValueTypes */ /** * An YXmlElement imitates the behavior of a * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}. * * * An YXmlElement has attributes (key value pairs) * * An YXmlElement has childElements that must inherit from YXmlElement * * @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }] */ export class YXmlElement extends YXmlFragment { constructor (nodeName = 'UNDEFINED') { super() this.nodeName = nodeName /** * @type {Map|null} */ this._prelimAttrs = new Map() } /** * @type {YXmlElement|YXmlText|null} */ get nextSibling () { const n = this._item ? this._item.next : null return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null } /** * @type {YXmlElement|YXmlText|null} */ get prevSibling () { const n = this._item ? this._item.prev : null return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate (y, item) { super._integrate(y, item) ;(/** @type {Map} */ (this._prelimAttrs)).forEach((value, key) => { this.setAttribute(key, value) }) this._prelimAttrs = null } /** * Creates an Item with the same effect as this Item (without position effect) * * @return {YXmlElement} */ _copy () { return new YXmlElement(this.nodeName) } /** * @return {YXmlElement} */ clone () { /** * @type {YXmlElement} */ const el = new YXmlElement(this.nodeName) const attrs = this.getAttributes() object.forEach(attrs, (value, key) => { if (typeof value === 'string') { el.setAttribute(key, value) } }) // @ts-ignore el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) return el } /** * Returns the XML serialization of this YXmlElement. * The attributes are ordered by attribute-name, so you can easily use this * method to compare YXmlElements * * @return {string} The string representation of this type. * * @public */ toString () { const attrs = this.getAttributes() const stringBuilder = [] const keys = [] for (const key in attrs) { keys.push(key) } keys.sort() const keysLen = keys.length for (let i = 0; i < keysLen; i++) { const key = keys[i] stringBuilder.push(key + '="' + attrs[key] + '"') } const nodeName = this.nodeName.toLocaleLowerCase() const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : '' return `<${nodeName}${attrsString}>${super.toString()}` } /** * Removes an attribute from this YXmlElement. * * @param {string} attributeName The attribute name that is to be removed. * * @public */ removeAttribute (attributeName) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapDelete(transaction, this, attributeName) }) } else { /** @type {Map} */ (this._prelimAttrs).delete(attributeName) } } /** * Sets or updates an attribute. * * @template {keyof KV & string} KEY * * @param {KEY} attributeName The attribute name that is to be set. * @param {KV[KEY]} attributeValue The attribute value that is to be set. * * @public */ setAttribute (attributeName, attributeValue) { if (this.doc !== null) { transact(this.doc, transaction => { typeMapSet(transaction, this, attributeName, attributeValue) }) } else { /** @type {Map} */ (this._prelimAttrs).set(attributeName, attributeValue) } } /** * Returns an attribute value that belongs to the attribute name. * * @template {keyof KV & string} KEY * * @param {KEY} attributeName The attribute name that identifies the * queried value. * @return {KV[KEY]|undefined} The queried attribute value. * * @public */ getAttribute (attributeName) { return /** @type {any} */ (typeMapGet(this, attributeName)) } /** * Returns whether an attribute exists * * @param {string} attributeName The attribute name to check for existence. * @return {boolean} whether the attribute exists. * * @public */ hasAttribute (attributeName) { return /** @type {any} */ (typeMapHas(this, attributeName)) } /** * Returns all attribute name/value pairs in a JSON Object. * * @return {{ [Key in Extract]?: KV[Key]}} A JSON Object that describes the attributes. * * @public */ getAttributes () { return /** @type {any} */ (typeMapGetAll(this)) } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM (_document = document, hooks = {}, binding) { const dom = _document.createElement(this.nodeName) const attrs = this.getAttributes() for (const key in attrs) { const value = attrs[key] if (typeof value === 'string') { dom.setAttribute(key, value) } } typeListForEach(this, yxml => { dom.appendChild(yxml.toDOM(_document, hooks, binding)) }) if (binding !== undefined) { binding._createAssociation(dom, this) } return dom } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write (encoder) { encoder.writeTypeRef(YXmlElementRefID) encoder.writeKey(this.nodeName) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlElement} * * @function */ export const readYXmlElement = decoder => new YXmlElement(decoder.readKey()) yjs-13.6.8/src/types/YXmlEvent.js000066400000000000000000000022101450200430400165650ustar00rootroot00000000000000 import { YEvent, YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line } from '../internals.js' /** * @extends YEvent * An Event that describes changes on a YXml Element or Yxml Fragment */ export class YXmlEvent extends YEvent { /** * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created. * @param {Set} subs The set of changed attributes. `null` is included if the * child list changed. * @param {Transaction} transaction The transaction instance with wich the * change was created. */ constructor (target, subs, transaction) { super(target, transaction) /** * Whether the children changed. * @type {Boolean} * @private */ this.childListChanged = false /** * Set of all changed attributes. * @type {Set} */ this.attributesChanged = new Set() subs.forEach((sub) => { if (sub === null) { this.childListChanged = true } else { this.attributesChanged.add(sub) } }) } } yjs-13.6.8/src/types/YXmlFragment.js000066400000000000000000000275241450200430400172660ustar00rootroot00000000000000/** * @module YXml */ import { YXmlEvent, YXmlElement, AbstractType, typeListMap, typeListForEach, typeListInsertGenerics, typeListInsertGenericsAfter, typeListDelete, typeListToArray, YXmlFragmentRefID, callTypeObservers, transact, typeListGet, typeListSlice, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' import * as array from 'lib0/array' /** * Define the elements to which a set of CSS queries apply. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors} * * @example * query = '.classSelector' * query = 'nodeSelector' * query = '#idSelector' * * @typedef {string} CSS_Selector */ /** * Dom filter function. * * @callback domFilter * @param {string} nodeName The nodeName of the element * @param {Map} attributes The map of attributes. * @return {boolean} Whether to include the Dom node in the YXmlElement. */ /** * Represents a subset of the nodes of a YXmlElement / YXmlFragment and a * position within them. * * Can be created with {@link YXmlFragment#createTreeWalker} * * @public * @implements {Iterable} */ export class YXmlTreeWalker { /** * @param {YXmlFragment | YXmlElement} root * @param {function(AbstractType):boolean} [f] */ constructor (root, f = () => true) { this._filter = f this._root = root /** * @type {Item} */ this._currentNode = /** @type {Item} */ (root._start) this._firstCall = true } [Symbol.iterator] () { return this } /** * Get the next node. * * @return {IteratorResult} The next node. * * @public */ next () { /** * @type {Item|null} */ let n = this._currentNode let type = n && n.content && /** @type {any} */ (n.content).type if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item do { type = /** @type {any} */ (n.content).type if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { // walk down in the tree n = type._start } else { // walk right or up in the tree while (n !== null) { if (n.right !== null) { n = n.right break } else if (n.parent === this._root) { n = null } else { n = /** @type {AbstractType} */ (n.parent)._item } } } } while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type))) } this._firstCall = false if (n === null) { // @ts-ignore return { value: undefined, done: true } } this._currentNode = n return { value: /** @type {any} */ (n.content).type, done: false } } } /** * Represents a list of {@link YXmlElement}.and {@link YXmlText} types. * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a * nodeName and it does not have attributes. Though it can be bound to a DOM * element - in this case the attributes and the nodeName are not shared. * * @public * @extends AbstractType */ export class YXmlFragment extends AbstractType { constructor () { super() /** * @type {Array|null} */ this._prelimContent = [] } /** * @type {YXmlElement|YXmlText|null} */ get firstChild () { const first = this._first return first ? first.content.getContent()[0] : null } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate (y, item) { super._integrate(y, item) this.insert(0, /** @type {Array} */ (this._prelimContent)) this._prelimContent = null } _copy () { return new YXmlFragment() } /** * @return {YXmlFragment} */ clone () { const el = new YXmlFragment() // @ts-ignore el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) return el } get length () { return this._prelimContent === null ? this._length : this._prelimContent.length } /** * Create a subtree of childNodes. * * @example * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') * for (let node in walker) { * // `node` is a div node * nop(node) * } * * @param {function(AbstractType):boolean} filter Function that is called on each child element and * returns a Boolean indicating whether the child * is to be included in the subtree. * @return {YXmlTreeWalker} A subtree and a position within it. * * @public */ createTreeWalker (filter) { return new YXmlTreeWalker(this, filter) } /** * Returns the first YXmlElement that matches the query. * Similar to DOM's {@link querySelector}. * * Query support: * - tagname * TODO: * - id * - attribute * * @param {CSS_Selector} query The query on the children. * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. * * @public */ querySelector (query) { query = query.toUpperCase() // @ts-ignore const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query) const next = iterator.next() if (next.done) { return null } else { return next.value } } /** * Returns all YXmlElements that match the query. * Similar to Dom's {@link querySelectorAll}. * * @todo Does not yet support all queries. Currently only query by tagName. * * @param {CSS_Selector} query The query on the children * @return {Array} The elements that match this query. * * @public */ querySelectorAll (query) { query = query.toUpperCase() // @ts-ignore return array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)) } /** * Creates YXmlEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver (transaction, parentSubs) { callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)) } /** * Get the string representation of all the children of this YXmlFragment. * * @return {string} The string representation of all children. */ toString () { return typeListMap(this, xml => xml.toString()).join('') } /** * @return {string} */ toJSON () { return this.toString() } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM (_document = document, hooks = {}, binding) { const fragment = _document.createDocumentFragment() if (binding !== undefined) { binding._createAssociation(fragment, this) } typeListForEach(this, xmlType => { fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null) }) return fragment } /** * Inserts new content at an index. * * @example * // Insert character 'a' at position 0 * xml.insert(0, [new Y.XmlText('text')]) * * @param {number} index The index to insert content at * @param {Array} content The array of content */ insert (index, content) { if (this.doc !== null) { transact(this.doc, transaction => { typeListInsertGenerics(transaction, this, index, content) }) } else { // @ts-ignore _prelimContent is defined because this is not yet integrated this._prelimContent.splice(index, 0, ...content) } } /** * Inserts new content at an index. * * @example * // Insert character 'a' at position 0 * xml.insert(0, [new Y.XmlText('text')]) * * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at * @param {Array} content The array of content */ insertAfter (ref, content) { if (this.doc !== null) { transact(this.doc, transaction => { const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref typeListInsertGenericsAfter(transaction, this, refItem, content) }) } else { const pc = /** @type {Array} */ (this._prelimContent) const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1 if (index === 0 && ref !== null) { throw error.create('Reference item not found') } pc.splice(index, 0, ...content) } } /** * Deletes elements starting from an index. * * @param {number} index Index at which to start deleting elements * @param {number} [length=1] The number of elements to remove. Defaults to 1. */ delete (index, length = 1) { if (this.doc !== null) { transact(this.doc, transaction => { typeListDelete(transaction, this, index, length) }) } else { // @ts-ignore _prelimContent is defined because this is not yet integrated this._prelimContent.splice(index, length) } } /** * Transforms this YArray to a JavaScript Array. * * @return {Array} */ toArray () { return typeListToArray(this) } /** * Appends content to this YArray. * * @param {Array} content Array of content to append. */ push (content) { this.insert(this.length, content) } /** * Preppends content to this YArray. * * @param {Array} content Array of content to preppend. */ unshift (content) { this.insert(0, content) } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray * @return {YXmlElement|YXmlText} */ get (index) { return typeListGet(this, index) } /** * Transforms this YArray to a JavaScript Array. * * @param {number} [start] * @param {number} [end] * @return {Array} */ slice (start = 0, end = this.length) { return typeListSlice(this, start, end) } /** * Executes a provided function on once on overy child element. * * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. */ forEach (f) { typeListForEach(this, f) } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write (encoder) { encoder.writeTypeRef(YXmlFragmentRefID) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * @return {YXmlFragment} * * @private * @function */ export const readYXmlFragment = _decoder => new YXmlFragment() yjs-13.6.8/src/types/YXmlHook.js000066400000000000000000000047161450200430400164210ustar00rootroot00000000000000 import { YMap, YXmlHookRefID, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line } from '../internals.js' /** * You can manage binding to a custom type with YXmlHook. * * @extends {YMap} */ export class YXmlHook extends YMap { /** * @param {string} hookName nodeName of the Dom Node. */ constructor (hookName) { super() /** * @type {string} */ this.hookName = hookName } /** * Creates an Item with the same effect as this Item (without position effect) */ _copy () { return new YXmlHook(this.hookName) } /** * @return {YXmlHook} */ clone () { const el = new YXmlHook(this.hookName) this.forEach((value, key) => { el.set(key, value) }) return el } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object.} [hooks] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM (_document = document, hooks = {}, binding) { const hook = hooks[this.hookName] let dom if (hook !== undefined) { dom = hook.createDom(this) } else { dom = document.createElement(this.hookName) } dom.setAttribute('data-yjs-hook', this.hookName) if (binding !== undefined) { binding._createAssociation(dom, this) } return dom } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write (encoder) { encoder.writeTypeRef(YXmlHookRefID) encoder.writeKey(this.hookName) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlHook} * * @private * @function */ export const readYXmlHook = decoder => new YXmlHook(decoder.readKey()) yjs-13.6.8/src/types/YXmlText.js000066400000000000000000000066371450200430400164510ustar00rootroot00000000000000 import { YText, YXmlTextRefID, ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line } from '../internals.js' /** * Represents text in a Dom Element. In the future this type will also handle * simple formatting information like bold and italic. */ export class YXmlText extends YText { /** * @type {YXmlElement|YXmlText|null} */ get nextSibling () { const n = this._item ? this._item.next : null return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null } /** * @type {YXmlElement|YXmlText|null} */ get prevSibling () { const n = this._item ? this._item.prev : null return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null } _copy () { return new YXmlText() } /** * @return {YXmlText} */ clone () { const text = new YXmlText() text.applyDelta(this.toDelta()) return text } /** * Creates a Dom Element that mirrors this YXmlText. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM (_document = document, hooks, binding) { const dom = _document.createTextNode(this.toString()) if (binding !== undefined) { binding._createAssociation(dom, this) } return dom } toString () { // @ts-ignore return this.toDelta().map(delta => { const nestedNodes = [] for (const nodeName in delta.attributes) { const attrs = [] for (const key in delta.attributes[nodeName]) { attrs.push({ key, value: delta.attributes[nodeName][key] }) } // sort attributes to get a unique order attrs.sort((a, b) => a.key < b.key ? -1 : 1) nestedNodes.push({ nodeName, attrs }) } // sort node order to get a unique order nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1) // now convert to dom string let str = '' for (let i = 0; i < nestedNodes.length; i++) { const node = nestedNodes[i] str += `<${node.nodeName}` for (let j = 0; j < node.attrs.length; j++) { const attr = node.attrs[j] str += ` ${attr.key}="${attr.value}"` } str += '>' } str += delta.insert for (let i = nestedNodes.length - 1; i >= 0; i--) { str += `` } return str }).join('') } /** * @return {string} */ toJSON () { return this.toString() } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write (encoder) { encoder.writeTypeRef(YXmlTextRefID) } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlText} * * @private * @function */ export const readYXmlText = decoder => new YXmlText() yjs-13.6.8/src/utils/000077500000000000000000000000001450200430400143355ustar00rootroot00000000000000yjs-13.6.8/src/utils/AbstractConnector.js000066400000000000000000000011461450200430400203130ustar00rootroot00000000000000 import { Observable } from 'lib0/observable' import { Doc // eslint-disable-line } from '../internals.js' /** * This is an abstract interface that all Connectors should implement to keep them interchangeable. * * @note This interface is experimental and it is not advised to actually inherit this class. * It just serves as typing information. * * @extends {Observable} */ export class AbstractConnector extends Observable { /** * @param {Doc} ydoc * @param {any} awareness */ constructor (ydoc, awareness) { super() this.doc = ydoc this.awareness = awareness } } yjs-13.6.8/src/utils/DeleteSet.js000066400000000000000000000235441450200430400165610ustar00rootroot00000000000000 import { findIndexSS, getState, splitItem, iterateStructs, UpdateEncoderV2, DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line } from '../internals.js' import * as array from 'lib0/array' import * as math from 'lib0/math' import * as map from 'lib0/map' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' export class DeleteItem { /** * @param {number} clock * @param {number} len */ constructor (clock, len) { /** * @type {number} */ this.clock = clock /** * @type {number} */ this.len = len } } /** * We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed. * - When created in a transaction, it must only be accessed after sorting, and merging * - This DeleteSet is send to other clients * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged. */ export class DeleteSet { constructor () { /** * @type {Map>} */ this.clients = new Map() } } /** * Iterate over all structs that the DeleteSet gc's. * * @param {Transaction} transaction * @param {DeleteSet} ds * @param {function(GC|Item):void} f * * @function */ export const iterateDeletedStructs = (transaction, ds, f) => ds.clients.forEach((deletes, clientid) => { const structs = /** @type {Array} */ (transaction.doc.store.clients.get(clientid)) for (let i = 0; i < deletes.length; i++) { const del = deletes[i] iterateStructs(transaction, structs, del.clock, del.len, f) } }) /** * @param {Array} dis * @param {number} clock * @return {number|null} * * @private * @function */ export const findIndexDS = (dis, clock) => { let left = 0 let right = dis.length - 1 while (left <= right) { const midindex = math.floor((left + right) / 2) const mid = dis[midindex] const midclock = mid.clock if (midclock <= clock) { if (clock < midclock + mid.len) { return midindex } left = midindex + 1 } else { right = midindex - 1 } } return null } /** * @param {DeleteSet} ds * @param {ID} id * @return {boolean} * * @private * @function */ export const isDeleted = (ds, id) => { const dis = ds.clients.get(id.client) return dis !== undefined && findIndexDS(dis, id.clock) !== null } /** * @param {DeleteSet} ds * * @private * @function */ export const sortAndMergeDeleteSet = ds => { ds.clients.forEach(dels => { dels.sort((a, b) => a.clock - b.clock) // merge items without filtering or splicing the array // i is the current pointer // j refers to the current insert position for the pointed item // try to merge dels[i] into dels[j-1] or set dels[j]=dels[i] let i, j for (i = 1, j = 1; i < dels.length; i++) { const left = dels[j - 1] const right = dels[i] if (left.clock + left.len >= right.clock) { left.len = math.max(left.len, right.clock + right.len - left.clock) } else { if (j < i) { dels[j] = right } j++ } } dels.length = j }) } /** * @param {Array} dss * @return {DeleteSet} A fresh DeleteSet */ export const mergeDeleteSets = dss => { const merged = new DeleteSet() for (let dssI = 0; dssI < dss.length; dssI++) { dss[dssI].clients.forEach((delsLeft, client) => { if (!merged.clients.has(client)) { // Write all missing keys from current ds and all following. // If merged already contains `client` current ds has already been added. /** * @type {Array} */ const dels = delsLeft.slice() for (let i = dssI + 1; i < dss.length; i++) { array.appendTo(dels, dss[i].clients.get(client) || []) } merged.clients.set(client, dels) } }) } sortAndMergeDeleteSet(merged) return merged } /** * @param {DeleteSet} ds * @param {number} client * @param {number} clock * @param {number} length * * @private * @function */ export const addToDeleteSet = (ds, client, clock, length) => { map.setIfUndefined(ds.clients, client, () => /** @type {Array} */ ([])).push(new DeleteItem(clock, length)) } export const createDeleteSet = () => new DeleteSet() /** * @param {StructStore} ss * @return {DeleteSet} Merged and sorted DeleteSet * * @private * @function */ export const createDeleteSetFromStructStore = ss => { const ds = createDeleteSet() ss.clients.forEach((structs, client) => { /** * @type {Array} */ const dsitems = [] for (let i = 0; i < structs.length; i++) { const struct = structs[i] if (struct.deleted) { const clock = struct.id.clock let len = struct.length if (i + 1 < structs.length) { for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { len += next.length } } dsitems.push(new DeleteItem(clock, len)) } } if (dsitems.length > 0) { ds.clients.set(client, dsitems) } }) return ds } /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {DeleteSet} ds * * @private * @function */ export const writeDeleteSet = (encoder, ds) => { encoding.writeVarUint(encoder.restEncoder, ds.clients.size) // Ensure that the delete set is written in a deterministic order array.from(ds.clients.entries()) .sort((a, b) => b[0] - a[0]) .forEach(([client, dsitems]) => { encoder.resetDsCurVal() encoding.writeVarUint(encoder.restEncoder, client) const len = dsitems.length encoding.writeVarUint(encoder.restEncoder, len) for (let i = 0; i < len; i++) { const item = dsitems[i] encoder.writeDsClock(item.clock) encoder.writeDsLen(item.len) } }) } /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @return {DeleteSet} * * @private * @function */ export const readDeleteSet = decoder => { const ds = new DeleteSet() const numClients = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal() const client = decoding.readVarUint(decoder.restDecoder) const numberOfDeletes = decoding.readVarUint(decoder.restDecoder) if (numberOfDeletes > 0) { const dsField = map.setIfUndefined(ds.clients, client, () => /** @type {Array} */ ([])) for (let i = 0; i < numberOfDeletes; i++) { dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen())) } } } return ds } /** * @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array().. */ /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @param {Transaction} transaction * @param {StructStore} store * @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully. * * @private * @function */ export const readAndApplyDeleteSet = (decoder, transaction, store) => { const unappliedDS = new DeleteSet() const numClients = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal() const client = decoding.readVarUint(decoder.restDecoder) const numberOfDeletes = decoding.readVarUint(decoder.restDecoder) const structs = store.clients.get(client) || [] const state = getState(store, client) for (let i = 0; i < numberOfDeletes; i++) { const clock = decoder.readDsClock() const clockEnd = clock + decoder.readDsLen() if (clock < state) { if (state < clockEnd) { addToDeleteSet(unappliedDS, client, state, clockEnd - state) } let index = findIndexSS(structs, clock) /** * We can ignore the case of GC and Delete structs, because we are going to skip them * @type {Item} */ // @ts-ignore let struct = structs[index] // split the first item if necessary if (!struct.deleted && struct.id.clock < clock) { structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)) index++ // increase we now want to use the next struct } while (index < structs.length) { // @ts-ignore struct = structs[index++] if (struct.id.clock < clockEnd) { if (!struct.deleted) { if (clockEnd < struct.id.clock + struct.length) { structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)) } struct.delete(transaction) } } else { break } } } else { addToDeleteSet(unappliedDS, client, clock, clockEnd - clock) } } } if (unappliedDS.clients.size > 0) { const ds = new UpdateEncoderV2() encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs writeDeleteSet(ds, unappliedDS) return ds.toUint8Array() } return null } /** * @param {DeleteSet} ds1 * @param {DeleteSet} ds2 */ export const equalDeleteSets = (ds1, ds2) => { if (ds1.clients.size !== ds2.clients.size) return false for (const [client, deleteItems1] of ds1.clients.entries()) { const deleteItems2 = /** @type {Array} */ (ds2.clients.get(client)) if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false for (let i = 0; i < deleteItems1.length; i++) { const di1 = deleteItems1[i] const di2 = deleteItems2[i] if (di1.clock !== di2.clock || di1.len !== di2.len) { return false } } } return true } yjs-13.6.8/src/utils/Doc.js000066400000000000000000000234361450200430400154100ustar00rootroot00000000000000/** * @module Y */ import { StructStore, AbstractType, YArray, YText, YMap, YXmlFragment, transact, ContentDoc, Item, Transaction, YEvent // eslint-disable-line } from '../internals.js' import { Observable } from 'lib0/observable' import * as random from 'lib0/random' import * as map from 'lib0/map' import * as array from 'lib0/array' import * as promise from 'lib0/promise' export const generateNewClientId = random.uint32 /** * @typedef {Object} DocOpts * @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true) * @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item. * @property {string} [DocOpts.guid] Define a globally unique identifier for this document * @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection. * @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well. * @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically. * @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load() */ /** * A Yjs instance handles the state of shared data. * @extends Observable */ export class Doc extends Observable { /** * @param {DocOpts} opts configuration */ constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { super() this.gc = gc this.gcFilter = gcFilter this.clientID = generateNewClientId() this.guid = guid this.collectionid = collectionid /** * @type {Map>>} */ this.share = new Map() this.store = new StructStore() /** * @type {Transaction | null} */ this._transaction = null /** * @type {Array} */ this._transactionCleanups = [] /** * @type {Set} */ this.subdocs = new Set() /** * If this document is a subdocument - a document integrated into another document - then _item is defined. * @type {Item?} */ this._item = null this.shouldLoad = shouldLoad this.autoLoad = autoLoad this.meta = meta /** * This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires. * Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database. * * @type {boolean} */ this.isLoaded = false /** * This is set to true when the connection provider has successfully synced with a backend. * Note that when using peer-to-peer providers this event may not provide very useful. * Also note that not all providers implement this feature. Provider authors are encouraged to fire * the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is * lost (with false as a parameter). */ this.isSynced = false /** * Promise that resolves once the document has been loaded from a presistence provider. */ this.whenLoaded = promise.create(resolve => { this.on('load', () => { this.isLoaded = true resolve(this) }) }) const provideSyncedPromise = () => promise.create(resolve => { /** * @param {boolean} isSynced */ const eventHandler = (isSynced) => { if (isSynced === undefined || isSynced === true) { this.off('sync', eventHandler) resolve() } } this.on('sync', eventHandler) }) this.on('sync', isSynced => { if (isSynced === false && this.isSynced) { this.whenSynced = provideSyncedPromise() } this.isSynced = isSynced === undefined || isSynced === true if (!this.isLoaded) { this.emit('load', []) } }) /** * Promise that resolves once the document has been synced with a backend. * This promise is recreated when the connection is lost. * Note the documentation about the `isSynced` property. */ this.whenSynced = provideSyncedPromise() } /** * Notify the parent document that you request to load data into this subdocument (if it is a subdocument). * * `load()` might be used in the future to request any provider to load the most current data. * * It is safe to call `load()` multiple times. */ load () { const item = this._item if (item !== null && !this.shouldLoad) { transact(/** @type {any} */ (item.parent).doc, transaction => { transaction.subdocsLoaded.add(this) }, null, true) } this.shouldLoad = true } getSubdocs () { return this.subdocs } getSubdocGuids () { return new Set(array.from(this.subdocs).map(doc => doc.guid)) } /** * Changes that happen inside of a transaction are bundled. This means that * the observer fires _after_ the transaction is finished and that all changes * that happened inside of the transaction are sent as one message to the * other peers. * * @template T * @param {function(Transaction):T} f The function that should be executed as a transaction * @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin * @return T * * @public */ transact (f, origin = null) { return transact(this, f, origin) } /** * Define a shared data type. * * Multiple calls of `y.get(name, TypeConstructor)` yield the same result * and do not overwrite each other. I.e. * `y.define(name, Y.Array) === y.define(name, Y.Array)` * * After this method is called, the type is also available on `y.share.get(name)`. * * *Best Practices:* * Define all types right after the Yjs instance is created and store them in a separate object. * Also use the typed methods `getText(name)`, `getArray(name)`, .. * * @example * const y = new Y(..) * const appState = { * document: y.getText('document') * comments: y.getArray('comments') * } * * @param {string} name * @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... * @return {AbstractType} The created type. Constructed with TypeConstructor * * @public */ get (name, TypeConstructor = AbstractType) { const type = map.setIfUndefined(this.share, name, () => { // @ts-ignore const t = new TypeConstructor() t._integrate(this, null) return t }) const Constr = type.constructor if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { if (Constr === AbstractType) { // @ts-ignore const t = new TypeConstructor() t._map = type._map type._map.forEach(/** @param {Item?} n */ n => { for (; n !== null; n = n.left) { // @ts-ignore n.parent = t } }) t._start = type._start for (let n = t._start; n !== null; n = n.right) { n.parent = t } t._length = type._length this.share.set(name, t) t._integrate(this, null) return t } else { throw new Error(`Type with the name ${name} has already been defined with a different constructor`) } } return type } /** * @template T * @param {string} [name] * @return {YArray} * * @public */ getArray (name = '') { // @ts-ignore return this.get(name, YArray) } /** * @param {string} [name] * @return {YText} * * @public */ getText (name = '') { // @ts-ignore return this.get(name, YText) } /** * @template T * @param {string} [name] * @return {YMap} * * @public */ getMap (name = '') { // @ts-ignore return this.get(name, YMap) } /** * @param {string} [name] * @return {YXmlFragment} * * @public */ getXmlFragment (name = '') { // @ts-ignore return this.get(name, YXmlFragment) } /** * Converts the entire document into a js object, recursively traversing each yjs type * Doesn't log types that have not been defined (using ydoc.getType(..)). * * @deprecated Do not use this method and rather call toJSON directly on the shared types. * * @return {Object} */ toJSON () { /** * @type {Object} */ const doc = {} this.share.forEach((value, key) => { doc[key] = value.toJSON() }) return doc } /** * Emit `destroy` event and unregister all event handlers. */ destroy () { array.from(this.subdocs).forEach(subdoc => subdoc.destroy()) const item = this._item if (item !== null) { this._item = null const content = /** @type {ContentDoc} */ (item.content) content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false }) content.doc._item = item transact(/** @type {any} */ (item).parent.doc, transaction => { const doc = content.doc if (!item.deleted) { transaction.subdocsAdded.add(doc) } transaction.subdocsRemoved.add(this) }, null, true) } this.emit('destroyed', [true]) this.emit('destroy', [this]) super.destroy() } /** * @param {string} eventName * @param {function(...any):any} f */ on (eventName, f) { super.on(eventName, f) } /** * @param {string} eventName * @param {function} f */ off (eventName, f) { super.off(eventName, f) } } yjs-13.6.8/src/utils/EventHandler.js000066400000000000000000000036061450200430400172570ustar00rootroot00000000000000import * as f from 'lib0/function' /** * General event handler implementation. * * @template ARG0, ARG1 * * @private */ export class EventHandler { constructor () { /** * @type {Array} */ this.l = [] } } /** * @template ARG0,ARG1 * @returns {EventHandler} * * @private * @function */ export const createEventHandler = () => new EventHandler() /** * Adds an event listener that is called when * {@link EventHandler#callEventListeners} is called. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {function(ARG0,ARG1):void} f The event handler. * * @private * @function */ export const addEventHandlerListener = (eventHandler, f) => eventHandler.l.push(f) /** * Removes an event listener. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {function(ARG0,ARG1):void} f The event handler that was added with * {@link EventHandler#addEventListener} * * @private * @function */ export const removeEventHandlerListener = (eventHandler, f) => { const l = eventHandler.l const len = l.length eventHandler.l = l.filter(g => f !== g) if (len === eventHandler.l.length) { console.error('[yjs] Tried to remove event handler that doesn\'t exist.') } } /** * Removes all event listeners. * @template ARG0,ARG1 * @param {EventHandler} eventHandler * * @private * @function */ export const removeAllEventHandlerListeners = eventHandler => { eventHandler.l.length = 0 } /** * Call all event listeners that were added via * {@link EventHandler#addEventListener}. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {ARG0} arg0 * @param {ARG1} arg1 * * @private * @function */ export const callEventHandlerListeners = (eventHandler, arg0, arg1) => f.callAll(eventHandler.l, [arg0, arg1]) yjs-13.6.8/src/utils/ID.js000066400000000000000000000037131450200430400151730ustar00rootroot00000000000000 import { AbstractType } from '../internals.js' // eslint-disable-line import * as decoding from 'lib0/decoding' import * as encoding from 'lib0/encoding' import * as error from 'lib0/error' export class ID { /** * @param {number} client client id * @param {number} clock unique per client id, continuous number */ constructor (client, clock) { /** * Client id * @type {number} */ this.client = client /** * unique per client id, continuous number * @type {number} */ this.clock = clock } } /** * @param {ID | null} a * @param {ID | null} b * @return {boolean} * * @function */ export const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock) /** * @param {number} client * @param {number} clock * * @private * @function */ export const createID = (client, clock) => new ID(client, clock) /** * @param {encoding.Encoder} encoder * @param {ID} id * * @private * @function */ export const writeID = (encoder, id) => { encoding.writeVarUint(encoder, id.client) encoding.writeVarUint(encoder, id.clock) } /** * Read ID. * * If first varUint read is 0xFFFFFF a RootID is returned. * * Otherwise an ID is returned * * @param {decoding.Decoder} decoder * @return {ID} * * @private * @function */ export const readID = decoder => createID(decoding.readVarUint(decoder), decoding.readVarUint(decoder)) /** * The top types are mapped from y.share.get(keyname) => type. * `type` does not store any information about the `keyname`. * This function finds the correct `keyname` for `type` and throws otherwise. * * @param {AbstractType} type * @return {string} * * @private * @function */ export const findRootTypeKey = type => { // @ts-ignore _y must be defined, otherwise unexpected case for (const [key, value] of type.doc.share.entries()) { if (value === type) { return key } } throw error.unexpectedCase() } yjs-13.6.8/src/utils/PermanentUserData.js000066400000000000000000000103401450200430400202530ustar00rootroot00000000000000 import { YArray, YMap, readDeleteSet, writeDeleteSet, createDeleteSet, DSEncoderV1, DSDecoderV1, ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line } from '../internals.js' import * as decoding from 'lib0/decoding' import { mergeDeleteSets, isDeleted } from './DeleteSet.js' export class PermanentUserData { /** * @param {Doc} doc * @param {YMap} [storeType] */ constructor (doc, storeType = doc.getMap('users')) { /** * @type {Map} */ const dss = new Map() this.yusers = storeType this.doc = doc /** * Maps from clientid to userDescription * * @type {Map} */ this.clients = new Map() this.dss = dss /** * @param {YMap} user * @param {string} userDescription */ const initUser = (user, userDescription) => { /** * @type {YArray} */ const ds = user.get('ds') const ids = user.get('ids') const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription) ds.observe(/** @param {YArrayEvent} event */ event => { event.changes.added.forEach(item => { item.content.getContent().forEach(encodedDs => { if (encodedDs instanceof Uint8Array) { this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(new DSDecoderV1(decoding.createDecoder(encodedDs)))])) } }) }) }) this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(new DSDecoderV1(decoding.createDecoder(encodedDs)))))) ids.observe(/** @param {YArrayEvent} event */ event => event.changes.added.forEach(item => item.content.getContent().forEach(addClientId)) ) ids.forEach(addClientId) } // observe users storeType.observe(event => { event.keysChanged.forEach(userDescription => initUser(storeType.get(userDescription), userDescription) ) }) // add intial data storeType.forEach(initUser) } /** * @param {Doc} doc * @param {number} clientid * @param {string} userDescription * @param {Object} conf * @param {function(Transaction, DeleteSet):boolean} [conf.filter] */ setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) { const users = this.yusers let user = users.get(userDescription) if (!user) { user = new YMap() user.set('ids', new YArray()) user.set('ds', new YArray()) users.set(userDescription, user) } user.get('ids').push([clientid]) users.observe(_event => { setTimeout(() => { const userOverwrite = users.get(userDescription) if (userOverwrite !== user) { // user was overwritten, port all data over to the next user object // @todo Experiment with Y.Sets here user = userOverwrite // @todo iterate over old type this.clients.forEach((_userDescription, clientid) => { if (userDescription === _userDescription) { user.get('ids').push([clientid]) } }) const encoder = new DSEncoderV1() const ds = this.dss.get(userDescription) if (ds) { writeDeleteSet(encoder, ds) user.get('ds').push([encoder.toUint8Array()]) } } }, 0) }) doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => { setTimeout(() => { const yds = user.get('ds') const ds = transaction.deleteSet if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) { const encoder = new DSEncoderV1() writeDeleteSet(encoder, ds) yds.push([encoder.toUint8Array()]) } }) }) } /** * @param {number} clientid * @return {any} */ getUserByClientId (clientid) { return this.clients.get(clientid) || null } /** * @param {ID} id * @return {string | null} */ getUserByDeletedId (id) { for (const [userDescription, ds] of this.dss.entries()) { if (isDeleted(ds, id)) { return userDescription } } return null } } yjs-13.6.8/src/utils/RelativePosition.js000066400000000000000000000214171450200430400202000ustar00rootroot00000000000000 import { writeID, readID, compareIDs, getState, findRootTypeKey, Item, createID, ContentType, followRedone, ID, Doc, AbstractType // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' import * as error from 'lib0/error' /** * A relative position is based on the Yjs model and is not affected by document changes. * E.g. If you place a relative position before a certain character, it will always point to this character. * If you place a relative position at the end of a type, it will always point to the end of the type. * * A numeric position is often unsuited for user selections, because it does not change when content is inserted * before or after. * * ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position. * * One of the properties must be defined. * * @example * // Current cursor position is at position 10 * const relativePosition = createRelativePositionFromIndex(yText, 10) * // modify yText * yText.insert(0, 'abc') * yText.delete(3, 10) * // Compute the cursor position * const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition) * absolutePosition.type === yText // => true * console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3 * */ export class RelativePosition { /** * @param {ID|null} type * @param {string|null} tname * @param {ID|null} item * @param {number} assoc */ constructor (type, tname, item, assoc = 0) { /** * @type {ID|null} */ this.type = type /** * @type {string|null} */ this.tname = tname /** * @type {ID | null} */ this.item = item /** * A relative position is associated to a specific character. By default * assoc >= 0, the relative position is associated to the character * after the meant position. * I.e. position 1 in 'ab' is associated to character 'b'. * * If assoc < 0, then the relative position is associated to the caharacter * before the meant position. * * @type {number} */ this.assoc = assoc } } /** * @param {RelativePosition} rpos * @return {any} */ export const relativePositionToJSON = rpos => { const json = {} if (rpos.type) { json.type = rpos.type } if (rpos.tname) { json.tname = rpos.tname } if (rpos.item) { json.item = rpos.item } if (rpos.assoc != null) { json.assoc = rpos.assoc } return json } /** * @param {any} json * @return {RelativePosition} * * @function */ export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc) export class AbsolutePosition { /** * @param {AbstractType} type * @param {number} index * @param {number} [assoc] */ constructor (type, index, assoc = 0) { /** * @type {AbstractType} */ this.type = type /** * @type {number} */ this.index = index this.assoc = assoc } } /** * @param {AbstractType} type * @param {number} index * @param {number} [assoc] * * @function */ export const createAbsolutePosition = (type, index, assoc = 0) => new AbsolutePosition(type, index, assoc) /** * @param {AbstractType} type * @param {ID|null} item * @param {number} [assoc] * * @function */ export const createRelativePosition = (type, item, assoc) => { let typeid = null let tname = null if (type._item === null) { tname = findRootTypeKey(type) } else { typeid = createID(type._item.id.client, type._item.id.clock) } return new RelativePosition(typeid, tname, item, assoc) } /** * Create a relativePosition based on a absolute position. * * @param {AbstractType} type The base type (e.g. YText or YArray). * @param {number} index The absolute position. * @param {number} [assoc] * @return {RelativePosition} * * @function */ export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => { let t = type._start if (assoc < 0) { // associated to the left character or the beginning of a type, increment index if possible. if (index === 0) { return createRelativePosition(type, null, assoc) } index-- } while (t !== null) { if (!t.deleted && t.countable) { if (t.length > index) { // case 1: found position somewhere in the linked list return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc) } index -= t.length } if (t.right === null && assoc < 0) { // left-associated position, return last available id return createRelativePosition(type, t.lastId, assoc) } t = t.right } return createRelativePosition(type, null, assoc) } /** * @param {encoding.Encoder} encoder * @param {RelativePosition} rpos * * @function */ export const writeRelativePosition = (encoder, rpos) => { const { type, tname, item, assoc } = rpos if (item !== null) { encoding.writeVarUint(encoder, 0) writeID(encoder, item) } else if (tname !== null) { // case 2: found position at the end of the list and type is stored in y.share encoding.writeUint8(encoder, 1) encoding.writeVarString(encoder, tname) } else if (type !== null) { // case 3: found position at the end of the list and type is attached to an item encoding.writeUint8(encoder, 2) writeID(encoder, type) } else { throw error.unexpectedCase() } encoding.writeVarInt(encoder, assoc) return encoder } /** * @param {RelativePosition} rpos * @return {Uint8Array} */ export const encodeRelativePosition = rpos => { const encoder = encoding.createEncoder() writeRelativePosition(encoder, rpos) return encoding.toUint8Array(encoder) } /** * @param {decoding.Decoder} decoder * @return {RelativePosition} * * @function */ export const readRelativePosition = decoder => { let type = null let tname = null let itemID = null switch (decoding.readVarUint(decoder)) { case 0: // case 1: found position somewhere in the linked list itemID = readID(decoder) break case 1: // case 2: found position at the end of the list and type is stored in y.share tname = decoding.readVarString(decoder) break case 2: { // case 3: found position at the end of the list and type is attached to an item type = readID(decoder) } } const assoc = decoding.hasContent(decoder) ? decoding.readVarInt(decoder) : 0 return new RelativePosition(type, tname, itemID, assoc) } /** * @param {Uint8Array} uint8Array * @return {RelativePosition} */ export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array)) /** * @param {RelativePosition} rpos * @param {Doc} doc * @return {AbsolutePosition|null} * * @function */ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { const store = doc.store const rightID = rpos.item const typeID = rpos.type const tname = rpos.tname const assoc = rpos.assoc let type = null let index = 0 if (rightID !== null) { if (getState(store, rightID.client) <= rightID.clock) { return null } const res = followRedone(store, rightID) const right = res.item if (!(right instanceof Item)) { return null } type = /** @type {AbstractType} */ (right.parent) if (type._item === null || !type._item.deleted) { index = (right.deleted || !right.countable) ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary let n = right.left while (n !== null) { if (!n.deleted && n.countable) { index += n.length } n = n.left } } } else { if (tname !== null) { type = doc.get(tname) } else if (typeID !== null) { if (getState(store, typeID.client) <= typeID.clock) { // type does not exist yet return null } const { item } = followRedone(store, typeID) if (item instanceof Item && item.content instanceof ContentType) { type = item.content.type } else { // struct is garbage collected return null } } else { throw error.unexpectedCase() } if (assoc >= 0) { index = type._length } else { index = 0 } } return createAbsolutePosition(type, index, rpos.assoc) } /** * @param {RelativePosition|null} a * @param {RelativePosition|null} b * @return {boolean} * * @function */ export const compareRelativePositions = (a, b) => a === b || ( a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type) && a.assoc === b.assoc ) yjs-13.6.8/src/utils/Snapshot.js000066400000000000000000000152541450200430400165010ustar00rootroot00000000000000 import { isDeleted, createDeleteSetFromStructStore, getStateVector, getItemCleanStart, iterateDeletedStructs, writeDeleteSet, writeStateVector, readDeleteSet, readStateVector, createDeleteSet, createID, getState, findIndexSS, UpdateEncoderV2, applyUpdateV2, LazyStructReader, equalDeleteSets, UpdateDecoderV1, UpdateDecoderV2, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item, // eslint-disable-line mergeDeleteSets } from '../internals.js' import * as map from 'lib0/map' import * as set from 'lib0/set' import * as decoding from 'lib0/decoding' import * as encoding from 'lib0/encoding' export class Snapshot { /** * @param {DeleteSet} ds * @param {Map} sv state map */ constructor (ds, sv) { /** * @type {DeleteSet} */ this.ds = ds /** * State Map * @type {Map} */ this.sv = sv } } /** * @param {Snapshot} snap1 * @param {Snapshot} snap2 * @return {boolean} */ export const equalSnapshots = (snap1, snap2) => { const ds1 = snap1.ds.clients const ds2 = snap2.ds.clients const sv1 = snap1.sv const sv2 = snap2.sv if (sv1.size !== sv2.size || ds1.size !== ds2.size) { return false } for (const [key, value] of sv1.entries()) { if (sv2.get(key) !== value) { return false } } for (const [client, dsitems1] of ds1.entries()) { const dsitems2 = ds2.get(client) || [] if (dsitems1.length !== dsitems2.length) { return false } for (let i = 0; i < dsitems1.length; i++) { const dsitem1 = dsitems1[i] const dsitem2 = dsitems2[i] if (dsitem1.clock !== dsitem2.clock || dsitem1.len !== dsitem2.len) { return false } } } return true } /** * @param {Snapshot} snapshot * @param {DSEncoderV1 | DSEncoderV2} [encoder] * @return {Uint8Array} */ export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => { writeDeleteSet(encoder, snapshot.ds) writeStateVector(encoder, snapshot.sv) return encoder.toUint8Array() } /** * @param {Snapshot} snapshot * @return {Uint8Array} */ export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DSEncoderV1()) /** * @param {Uint8Array} buf * @param {DSDecoderV1 | DSDecoderV2} [decoder] * @return {Snapshot} */ export const decodeSnapshotV2 = (buf, decoder = new DSDecoderV2(decoding.createDecoder(buf))) => { return new Snapshot(readDeleteSet(decoder), readStateVector(decoder)) } /** * @param {Uint8Array} buf * @return {Snapshot} */ export const decodeSnapshot = buf => decodeSnapshotV2(buf, new DSDecoderV1(decoding.createDecoder(buf))) /** * @param {DeleteSet} ds * @param {Map} sm * @return {Snapshot} */ export const createSnapshot = (ds, sm) => new Snapshot(ds, sm) export const emptySnapshot = createSnapshot(createDeleteSet(), new Map()) /** * @param {Doc} doc * @return {Snapshot} */ export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store)) /** * @param {Item} item * @param {Snapshot|undefined} snapshot * * @protected * @function */ export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id) /** * @param {Transaction} transaction * @param {Snapshot} snapshot */ export const splitSnapshotAffectedStructs = (transaction, snapshot) => { const meta = map.setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, set.create) const store = transaction.doc.store // check if we already split for this snapshot if (!meta.has(snapshot)) { snapshot.sv.forEach((clock, client) => { if (clock < getState(store, client)) { getItemCleanStart(transaction, createID(client, clock)) } }) iterateDeletedStructs(transaction, snapshot.ds, _item => {}) meta.add(snapshot) } } /** * @example * const ydoc = new Y.Doc({ gc: false }) * ydoc.getText().insert(0, 'world!') * const snapshot = Y.snapshot(ydoc) * ydoc.getText().insert(0, 'hello ') * const restored = Y.createDocFromSnapshot(ydoc, snapshot) * assert(restored.getText().toString() === 'world!') * * @param {Doc} originDoc * @param {Snapshot} snapshot * @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc * @return {Doc} */ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => { if (originDoc.gc) { // we should not try to restore a GC-ed document, because some of the restored items might have their content deleted throw new Error('Garbage-collection must be disabled in `originDoc`!') } const { sv, ds } = snapshot const encoder = new UpdateEncoderV2() originDoc.transact(transaction => { let size = 0 sv.forEach(clock => { if (clock > 0) { size++ } }) encoding.writeVarUint(encoder.restEncoder, size) // splitting the structs before writing them to the encoder for (const [client, clock] of sv) { if (clock === 0) { continue } if (clock < getState(originDoc.store, client)) { getItemCleanStart(transaction, createID(client, clock)) } const structs = originDoc.store.clients.get(client) || [] const lastStructIndex = findIndexSS(structs, clock - 1) // write # encoded structs encoding.writeVarUint(encoder.restEncoder, lastStructIndex + 1) encoder.writeClient(client) // first clock written is 0 encoding.writeVarUint(encoder.restEncoder, 0) for (let i = 0; i <= lastStructIndex; i++) { structs[i].write(encoder, 0) } } writeDeleteSet(encoder, ds) }) applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot') return newDoc } /** * @param {Snapshot} snapshot * @param {Uint8Array} update * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder] */ export const snapshotContainsUpdateV2 = (snapshot, update, YDecoder = UpdateDecoderV2) => { const structs = [] const updateDecoder = new YDecoder(decoding.createDecoder(update)) const lazyDecoder = new LazyStructReader(updateDecoder, false) for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { structs.push(curr) if ((snapshot.sv.get(curr.id.client) || 0) < curr.id.clock + curr.length) { return false } } const mergedDS = mergeDeleteSets([snapshot.ds, readDeleteSet(updateDecoder)]) return equalDeleteSets(snapshot.ds, mergedDS) } /** * @param {Snapshot} snapshot * @param {Uint8Array} update */ export const snapshotContainsUpdate = (snapshot, update) => snapshotContainsUpdateV2(snapshot, update, UpdateDecoderV1) yjs-13.6.8/src/utils/StructStore.js000066400000000000000000000147141450200430400172030ustar00rootroot00000000000000 import { GC, splitItem, Transaction, ID, Item, DSDecoderV2 // eslint-disable-line } from '../internals.js' import * as math from 'lib0/math' import * as error from 'lib0/error' export class StructStore { constructor () { /** * @type {Map>} */ this.clients = new Map() /** * @type {null | { missing: Map, update: Uint8Array }} */ this.pendingStructs = null /** * @type {null | Uint8Array} */ this.pendingDs = null } } /** * Return the states as a Map. * Note that clock refers to the next expected clock id. * * @param {StructStore} store * @return {Map} * * @public * @function */ export const getStateVector = store => { const sm = new Map() store.clients.forEach((structs, client) => { const struct = structs[structs.length - 1] sm.set(client, struct.id.clock + struct.length) }) return sm } /** * @param {StructStore} store * @param {number} client * @return {number} * * @public * @function */ export const getState = (store, client) => { const structs = store.clients.get(client) if (structs === undefined) { return 0 } const lastStruct = structs[structs.length - 1] return lastStruct.id.clock + lastStruct.length } /** * @param {StructStore} store * * @private * @function */ export const integretyCheck = store => { store.clients.forEach(structs => { for (let i = 1; i < structs.length; i++) { const l = structs[i - 1] const r = structs[i] if (l.id.clock + l.length !== r.id.clock) { throw new Error('StructStore failed integrety check') } } }) } /** * @param {StructStore} store * @param {GC|Item} struct * * @private * @function */ export const addStruct = (store, struct) => { let structs = store.clients.get(struct.id.client) if (structs === undefined) { structs = [] store.clients.set(struct.id.client, structs) } else { const lastStruct = structs[structs.length - 1] if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) { throw error.unexpectedCase() } } structs.push(struct) } /** * Perform a binary search on a sorted array * @param {Array} structs * @param {number} clock * @return {number} * * @private * @function */ export const findIndexSS = (structs, clock) => { let left = 0 let right = structs.length - 1 let mid = structs[right] let midclock = mid.id.clock if (midclock === clock) { return right } // @todo does it even make sense to pivot the search? // If a good split misses, it might actually increase the time to find the correct item. // Currently, the only advantage is that search with pivoting might find the item on the first try. let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search while (left <= right) { mid = structs[midindex] midclock = mid.id.clock if (midclock <= clock) { if (clock < midclock + mid.length) { return midindex } left = midindex + 1 } else { right = midindex - 1 } midindex = math.floor((left + right) / 2) } // Always check state before looking for a struct in StructStore // Therefore the case of not finding a struct is unexpected throw error.unexpectedCase() } /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {StructStore} store * @param {ID} id * @return {GC|Item} * * @private * @function */ export const find = (store, id) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(id.client) return structs[findIndexSS(structs, id.clock)] } /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * @private * @function */ export const getItem = /** @type {function(StructStore,ID):Item} */ (find) /** * @param {Transaction} transaction * @param {Array} structs * @param {number} clock */ export const findIndexCleanStart = (transaction, structs, clock) => { const index = findIndexSS(structs, clock) const struct = structs[index] if (struct.id.clock < clock && struct instanceof Item) { structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)) return index + 1 } return index } /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {Transaction} transaction * @param {ID} id * @return {Item} * * @private * @function */ export const getItemCleanStart = (transaction, id) => { const structs = /** @type {Array} */ (transaction.doc.store.clients.get(id.client)) return structs[findIndexCleanStart(transaction, structs, id.clock)] } /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {Transaction} transaction * @param {StructStore} store * @param {ID} id * @return {Item} * * @private * @function */ export const getItemCleanEnd = (transaction, store, id) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(id.client) const index = findIndexSS(structs, id.clock) const struct = structs[index] if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1)) } return struct } /** * Replace `item` with `newitem` in store * @param {StructStore} store * @param {GC|Item} struct * @param {GC|Item} newStruct * * @private * @function */ export const replaceStruct = (store, struct, newStruct) => { const structs = /** @type {Array} */ (store.clients.get(struct.id.client)) structs[findIndexSS(structs, struct.id.clock)] = newStruct } /** * Iterate over a range of structs * * @param {Transaction} transaction * @param {Array} structs * @param {number} clockStart Inclusive start * @param {number} len * @param {function(GC|Item):void} f * * @function */ export const iterateStructs = (transaction, structs, clockStart, len, f) => { if (len === 0) { return } const clockEnd = clockStart + len let index = findIndexCleanStart(transaction, structs, clockStart) let struct do { struct = structs[index++] if (clockEnd < struct.id.clock + struct.length) { findIndexCleanStart(transaction, structs, clockEnd) } f(struct) } while (index < structs.length && structs[index].id.clock < clockEnd) } yjs-13.6.8/src/utils/Transaction.js000066400000000000000000000355001450200430400171630ustar00rootroot00000000000000 import { getState, writeStructsFromTransaction, writeDeleteSet, DeleteSet, sortAndMergeDeleteSet, getStateVector, findIndexSS, callEventHandlerListeners, Item, generateNewClientId, createID, cleanupYTextAfterTransaction, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map' import * as math from 'lib0/math' import * as set from 'lib0/set' import * as logging from 'lib0/logging' import { callAll } from 'lib0/function' /** * A transaction is created for every change on the Yjs model. It is possible * to bundle changes on the Yjs model in a single transaction to * minimize the number on messages sent and the number of observer calls. * If possible the user of this library should bundle as many changes as * possible. Here is an example to illustrate the advantages of bundling: * * @example * const map = y.define('map', YMap) * // Log content when change is triggered * map.observe(() => { * console.log('change triggered') * }) * // Each change on the map type triggers a log message: * map.set('a', 0) // => "change triggered" * map.set('b', 0) // => "change triggered" * // When put in a transaction, it will trigger the log after the transaction: * y.transact(() => { * map.set('a', 1) * map.set('b', 1) * }) // => "change triggered" * * @public */ export class Transaction { /** * @param {Doc} doc * @param {any} origin * @param {boolean} local */ constructor (doc, origin, local) { /** * The Yjs instance. * @type {Doc} */ this.doc = doc /** * Describes the set of deleted items by ids * @type {DeleteSet} */ this.deleteSet = new DeleteSet() /** * Holds the state before the transaction started. * @type {Map} */ this.beforeState = getStateVector(doc.store) /** * Holds the state after the transaction. * @type {Map} */ this.afterState = new Map() /** * All types that were directly modified (property added or child * inserted/deleted). New types are not included in this Set. * Maps from type to parentSubs (`item.parentSub = null` for YArray) * @type {Map>,Set>} */ this.changed = new Map() /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. * @type {Map>,Array>>} */ this.changedParentTypes = new Map() /** * @type {Array} */ this._mergeStructs = [] /** * @type {any} */ this.origin = origin /** * Stores meta information on the transaction * @type {Map} */ this.meta = new Map() /** * Whether this change originates from this doc. * @type {boolean} */ this.local = local /** * @type {Set} */ this.subdocsAdded = new Set() /** * @type {Set} */ this.subdocsRemoved = new Set() /** * @type {Set} */ this.subdocsLoaded = new Set() /** * @type {boolean} */ this._needFormattingCleanup = false } } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Transaction} transaction * @return {boolean} Whether data was written. */ export const writeUpdateMessageFromTransaction = (encoder, transaction) => { if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) { return false } sortAndMergeDeleteSet(transaction.deleteSet) writeStructsFromTransaction(encoder, transaction) writeDeleteSet(encoder, transaction.deleteSet) return true } /** * @param {Transaction} transaction * * @private * @function */ export const nextID = transaction => { const y = transaction.doc return createID(y.clientID, getState(y.store, y.clientID)) } /** * If `type.parent` was added in current transaction, `type` technically * did not change, it was just added and we should not fire events for `type`. * * @param {Transaction} transaction * @param {AbstractType>} type * @param {string|null} parentSub */ export const addChangedTypeToTransaction = (transaction, type, parentSub) => { const item = type._item if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) { map.setIfUndefined(transaction.changed, type, set.create).add(parentSub) } } /** * @param {Array} structs * @param {number} pos * @return {number} # of merged structs */ const tryToMergeWithLefts = (structs, pos) => { let right = structs[pos] let left = structs[pos - 1] let i = pos for (; i > 0; right = left, left = structs[--i - 1]) { if (left.deleted === right.deleted && left.constructor === right.constructor) { if (left.mergeWith(right)) { if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType} */ (right.parent)._map.get(right.parentSub) === right) { /** @type {AbstractType} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left)) } continue } } break } const merged = pos - i if (merged) { // remove all merged structs from the array structs.splice(pos + 1 - merged, merged) } return merged } /** * @param {DeleteSet} ds * @param {StructStore} store * @param {function(Item):boolean} gcFilter */ const tryGcDeleteSet = (ds, store, gcFilter) => { for (const [client, deleteItems] of ds.clients.entries()) { const structs = /** @type {Array} */ (store.clients.get(client)) for (let di = deleteItems.length - 1; di >= 0; di--) { const deleteItem = deleteItems[di] const endDeleteItemClock = deleteItem.clock + deleteItem.len for ( let si = findIndexSS(structs, deleteItem.clock), struct = structs[si]; si < structs.length && struct.id.clock < endDeleteItemClock; struct = structs[++si] ) { const struct = structs[si] if (deleteItem.clock + deleteItem.len <= struct.id.clock) { break } if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) { struct.gc(store, false) } } } } } /** * @param {DeleteSet} ds * @param {StructStore} store */ const tryMergeDeleteSet = (ds, store) => { // try to merge deleted / gc'd items // merge from right to left for better efficiecy and so we don't miss any merge targets ds.clients.forEach((deleteItems, client) => { const structs = /** @type {Array} */ (store.clients.get(client)) for (let di = deleteItems.length - 1; di >= 0; di--) { const deleteItem = deleteItems[di] // start with merging the item next to the last deleted item const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1)) for ( let si = mostRightIndexToCheck, struct = structs[si]; si > 0 && struct.id.clock >= deleteItem.clock; struct = structs[si] ) { si -= 1 + tryToMergeWithLefts(structs, si) } } }) } /** * @param {DeleteSet} ds * @param {StructStore} store * @param {function(Item):boolean} gcFilter */ export const tryGc = (ds, store, gcFilter) => { tryGcDeleteSet(ds, store, gcFilter) tryMergeDeleteSet(ds, store) } /** * @param {Array} transactionCleanups * @param {number} i */ const cleanupTransactions = (transactionCleanups, i) => { if (i < transactionCleanups.length) { const transaction = transactionCleanups[i] const doc = transaction.doc const store = doc.store const ds = transaction.deleteSet const mergeStructs = transaction._mergeStructs try { sortAndMergeDeleteSet(ds) transaction.afterState = getStateVector(transaction.doc.store) doc.emit('beforeObserverCalls', [transaction, doc]) /** * An array of event callbacks. * * Each callback is called even if the other ones throw errors. * * @type {Array} */ const fs = [] // observe events on changed types transaction.changed.forEach((subs, itemtype) => fs.push(() => { if (itemtype._item === null || !itemtype._item.deleted) { itemtype._callObserver(transaction, subs) } }) ) fs.push(() => { // deep observe events transaction.changedParentTypes.forEach((events, type) => { // We need to think about the possibility that the user transforms the // Y.Doc in the event. if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) { events = events .filter(event => event.target._item === null || !event.target._item.deleted ) events .forEach(event => { event.currentTarget = type // path is relative to the current target event._path = null }) // sort events by path length so that top-level events are fired first. events .sort((event1, event2) => event1.path.length - event2.path.length) // We don't need to check for events.length // because we know it has at least one element callEventHandlerListeners(type._dEH, events, transaction) } }) }) fs.push(() => doc.emit('afterTransaction', [transaction, doc])) callAll(fs, []) if (transaction._needFormattingCleanup) { cleanupYTextAfterTransaction(transaction) } } finally { // Replace deleted items with ItemDeleted / GC. // This is where content is actually remove from the Yjs Doc. if (doc.gc) { tryGcDeleteSet(ds, store, doc.gcFilter) } tryMergeDeleteSet(ds, store) // on all affected store.clients props, try to merge transaction.afterState.forEach((clock, client) => { const beforeClock = transaction.beforeState.get(client) || 0 if (beforeClock !== clock) { const structs = /** @type {Array} */ (store.clients.get(client)) // we iterate from right to left so we can safely remove entries const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) for (let i = structs.length - 1; i >= firstChangePos;) { i -= 1 + tryToMergeWithLefts(structs, i) } } }) // try to merge mergeStructs // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left // but at the moment DS does not handle duplicates for (let i = mergeStructs.length - 1; i >= 0; i--) { const { client, clock } = mergeStructs[i].id const structs = /** @type {Array} */ (store.clients.get(client)) const replacedStructPos = findIndexSS(structs, clock) if (replacedStructPos + 1 < structs.length) { if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) { continue // no need to perform next check, both are already merged } } if (replacedStructPos > 0) { tryToMergeWithLefts(structs, replacedStructPos) } } if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) { logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.') doc.clientID = generateNewClientId() } // @todo Merge all the transactions into one and provide send the data as a single update message doc.emit('afterTransactionCleanup', [transaction, doc]) if (doc._observers.has('update')) { const encoder = new UpdateEncoderV1() const hasContent = writeUpdateMessageFromTransaction(encoder, transaction) if (hasContent) { doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction]) } } if (doc._observers.has('updateV2')) { const encoder = new UpdateEncoderV2() const hasContent = writeUpdateMessageFromTransaction(encoder, transaction) if (hasContent) { doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction]) } } const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) { subdocsAdded.forEach(subdoc => { subdoc.clientID = doc.clientID if (subdoc.collectionid == null) { subdoc.collectionid = doc.collectionid } doc.subdocs.add(subdoc) }) subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc)) doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction]) subdocsRemoved.forEach(subdoc => subdoc.destroy()) } if (transactionCleanups.length <= i + 1) { doc._transactionCleanups = [] doc.emit('afterAllTransactions', [doc, transactionCleanups]) } else { cleanupTransactions(transactionCleanups, i + 1) } } } } /** * Implements the functionality of `y.transact(()=>{..})` * * @template T * @param {Doc} doc * @param {function(Transaction):T} f * @param {any} [origin=true] * @return {T} * * @function */ export const transact = (doc, f, origin = null, local = true) => { const transactionCleanups = doc._transactionCleanups let initialCall = false /** * @type {any} */ let result = null if (doc._transaction === null) { initialCall = true doc._transaction = new Transaction(doc, origin, local) transactionCleanups.push(doc._transaction) if (transactionCleanups.length === 1) { doc.emit('beforeAllTransactions', [doc]) } doc.emit('beforeTransaction', [doc._transaction, doc]) } try { result = f(doc._transaction) } finally { if (initialCall) { const finishCleanup = doc._transaction === transactionCleanups[0] doc._transaction = null if (finishCleanup) { // The first transaction ended, now process observer calls. // Observer call may create new transactions for which we need to call the observers and do cleanup. // We don't want to nest these calls, so we execute these calls one after // another. // Also we need to ensure that all cleanups are called, even if the // observes throw errors. // This file is full of hacky try {} finally {} blocks to ensure that an // event can throw errors and also that the cleanup is called. cleanupTransactions(transactionCleanups, 0) } } } return result } yjs-13.6.8/src/utils/UndoManager.js000066400000000000000000000301121450200430400170700ustar00rootroot00000000000000import { mergeDeleteSets, iterateDeletedStructs, keepItem, transact, createID, redoItem, isParentOf, followRedone, getItemCleanStart, isDeleted, addToDeleteSet, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line } from '../internals.js' import * as time from 'lib0/time' import * as array from 'lib0/array' import * as logging from 'lib0/logging' import { Observable } from 'lib0/observable' export class StackItem { /** * @param {DeleteSet} deletions * @param {DeleteSet} insertions */ constructor (deletions, insertions) { this.insertions = insertions this.deletions = deletions /** * Use this to save and restore metadata like selection range */ this.meta = new Map() } } /** * @param {Transaction} tr * @param {UndoManager} um * @param {StackItem} stackItem */ const clearUndoManagerStackItem = (tr, um, stackItem) => { iterateDeletedStructs(tr, stackItem.deletions, item => { if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) { keepItem(item, false) } }) } /** * @param {UndoManager} undoManager * @param {Array} stack * @param {string} eventType * @return {StackItem?} */ const popStackItem = (undoManager, stack, eventType) => { /** * Whether a change happened * @type {StackItem?} */ let result = null /** * Keep a reference to the transaction so we can fire the event with the changedParentTypes * @type {any} */ let _tr = null const doc = undoManager.doc const scope = undoManager.scope transact(doc, transaction => { while (stack.length > 0 && result === null) { const store = doc.store const stackItem = /** @type {StackItem} */ (stack.pop()) /** * @type {Set} */ const itemsToRedo = new Set() /** * @type {Array} */ const itemsToDelete = [] let performedChange = false iterateDeletedStructs(transaction, stackItem.insertions, struct => { if (struct instanceof Item) { if (struct.redone !== null) { let { item, diff } = followRedone(store, struct.id) if (diff > 0) { item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) } struct = item } if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { itemsToDelete.push(struct) } } }) iterateDeletedStructs(transaction, stackItem.deletions, struct => { if ( struct instanceof Item && scope.some(type => isParentOf(type, struct)) && // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval. !isDeleted(stackItem.insertions, struct.id) ) { itemsToRedo.add(struct) } }) itemsToRedo.forEach(struct => { performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange }) // We want to delete in reverse order so that children are deleted before // parents, so we have more information available when items are filtered. for (let i = itemsToDelete.length - 1; i >= 0; i--) { const item = itemsToDelete[i] if (undoManager.deleteFilter(item)) { item.delete(transaction) performedChange = true } } result = performedChange ? stackItem : null } transaction.changed.forEach((subProps, type) => { // destroy search marker if necessary if (subProps.has(null) && type._searchMarker) { type._searchMarker.length = 0 } }) _tr = transaction }, undoManager) if (result != null) { const changedParentTypes = _tr.changedParentTypes undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager]) } return result } /** * @typedef {Object} UndoManagerOptions * @property {number} [UndoManagerOptions.captureTimeout=500] * @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false. * @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes * it is necessary to filter what an Undo/Redo operation can delete. If this * filter returns false, the type/item won't be deleted even it is in the * undo/redo scope. * @property {Set} [UndoManagerOptions.trackedOrigins=new Set([null])] * @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..). * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. */ /** * Fires 'stack-item-added' event when a stack item was added to either the undo- or * the redo-stack. You may store additional stack information via the * metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties). * Fires 'stack-item-popped' event when a stack item was popped from either the * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`. * * @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>} */ export class UndoManager extends Observable { /** * @param {AbstractType|Array>} typeScope Accepts either a single type, or an array of types * @param {UndoManagerOptions} options */ constructor (typeScope, { captureTimeout = 500, captureTransaction = _tr => true, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false, doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc) } = {}) { super() /** * @type {Array>} */ this.scope = [] this.doc = doc this.addToScope(typeScope) this.deleteFilter = deleteFilter trackedOrigins.add(this) this.trackedOrigins = trackedOrigins this.captureTransaction = captureTransaction /** * @type {Array} */ this.undoStack = [] /** * @type {Array} */ this.redoStack = [] /** * Whether the client is currently undoing (calling UndoManager.undo) * * @type {boolean} */ this.undoing = false this.redoing = false this.lastChange = 0 this.ignoreRemoteMapChanges = ignoreRemoteMapChanges this.captureTimeout = captureTimeout /** * @param {Transaction} transaction */ this.afterTransactionHandler = transaction => { // Only track certain transactions if ( !this.captureTransaction(transaction) || !this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) ) { return } const undoing = this.undoing const redoing = this.redoing const stack = undoing ? this.redoStack : this.undoStack if (undoing) { this.stopCapturing() // next undo should not be appended to last stack item } else if (!redoing) { // neither undoing nor redoing: delete redoStack this.clear(false, true) } const insertions = new DeleteSet() transaction.afterState.forEach((endClock, client) => { const startClock = transaction.beforeState.get(client) || 0 const len = endClock - startClock if (len > 0) { addToDeleteSet(insertions, client, startClock, len) } }) const now = time.getUnixTime() let didAdd = false if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) { // append change to last stack op const lastOp = stack[stack.length - 1] lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet]) lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions]) } else { // create a new stack op stack.push(new StackItem(transaction.deleteSet, insertions)) didAdd = true } if (!undoing && !redoing) { this.lastChange = now } // make sure that deleted structs are not gc'd iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { keepItem(item, true) } }) const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this] if (didAdd) { this.emit('stack-item-added', changeEvent) } else { this.emit('stack-item-updated', changeEvent) } } this.doc.on('afterTransaction', this.afterTransactionHandler) this.doc.on('destroy', () => { this.destroy() }) } /** * @param {Array> | AbstractType} ytypes */ addToScope (ytypes) { ytypes = array.isArray(ytypes) ? ytypes : [ytypes] ytypes.forEach(ytype => { if (this.scope.every(yt => yt !== ytype)) { if (ytype.doc !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509 this.scope.push(ytype) } }) } /** * @param {any} origin */ addTrackedOrigin (origin) { this.trackedOrigins.add(origin) } /** * @param {any} origin */ removeTrackedOrigin (origin) { this.trackedOrigins.delete(origin) } clear (clearUndoStack = true, clearRedoStack = true) { if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) { this.doc.transact(tr => { if (clearUndoStack) { this.undoStack.forEach(item => clearUndoManagerStackItem(tr, this, item)) this.undoStack = [] } if (clearRedoStack) { this.redoStack.forEach(item => clearUndoManagerStackItem(tr, this, item)) this.redoStack = [] } this.emit('stack-cleared', [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }]) }) } } /** * UndoManager merges Undo-StackItem if they are created within time-gap * smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next * StackItem won't be merged. * * * @example * // without stopCapturing * ytext.insert(0, 'a') * ytext.insert(1, 'b') * um.undo() * ytext.toString() // => '' (note that 'ab' was removed) * // with stopCapturing * ytext.insert(0, 'a') * um.stopCapturing() * ytext.insert(0, 'b') * um.undo() * ytext.toString() // => 'a' (note that only 'b' was removed) * */ stopCapturing () { this.lastChange = 0 } /** * Undo last changes on type. * * @return {StackItem?} Returns StackItem if a change was applied */ undo () { this.undoing = true let res try { res = popStackItem(this, this.undoStack, 'undo') } finally { this.undoing = false } return res } /** * Redo last undo operation. * * @return {StackItem?} Returns StackItem if a change was applied */ redo () { this.redoing = true let res try { res = popStackItem(this, this.redoStack, 'redo') } finally { this.redoing = false } return res } /** * Are undo steps available? * * @return {boolean} `true` if undo is possible */ canUndo () { return this.undoStack.length > 0 } /** * Are redo steps available? * * @return {boolean} `true` if redo is possible */ canRedo () { return this.redoStack.length > 0 } destroy () { this.trackedOrigins.delete(this) this.doc.off('afterTransaction', this.afterTransactionHandler) super.destroy() } } yjs-13.6.8/src/utils/UpdateDecoder.js000066400000000000000000000136171450200430400174130ustar00rootroot00000000000000import * as buffer from 'lib0/buffer' import * as decoding from 'lib0/decoding' import { ID, createID } from '../internals.js' export class DSDecoderV1 { /** * @param {decoding.Decoder} decoder */ constructor (decoder) { this.restDecoder = decoder } resetDsCurVal () { // nop } /** * @return {number} */ readDsClock () { return decoding.readVarUint(this.restDecoder) } /** * @return {number} */ readDsLen () { return decoding.readVarUint(this.restDecoder) } } export class UpdateDecoderV1 extends DSDecoderV1 { /** * @return {ID} */ readLeftID () { return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder)) } /** * @return {ID} */ readRightID () { return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder)) } /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. */ readClient () { return decoding.readVarUint(this.restDecoder) } /** * @return {number} info An unsigned 8-bit integer */ readInfo () { return decoding.readUint8(this.restDecoder) } /** * @return {string} */ readString () { return decoding.readVarString(this.restDecoder) } /** * @return {boolean} isKey */ readParentInfo () { return decoding.readVarUint(this.restDecoder) === 1 } /** * @return {number} info An unsigned 8-bit integer */ readTypeRef () { return decoding.readVarUint(this.restDecoder) } /** * Write len of a struct - well suited for Opt RLE encoder. * * @return {number} len */ readLen () { return decoding.readVarUint(this.restDecoder) } /** * @return {any} */ readAny () { return decoding.readAny(this.restDecoder) } /** * @return {Uint8Array} */ readBuf () { return buffer.copyUint8Array(decoding.readVarUint8Array(this.restDecoder)) } /** * Legacy implementation uses JSON parse. We use any-decoding in v2. * * @return {any} */ readJSON () { return JSON.parse(decoding.readVarString(this.restDecoder)) } /** * @return {string} */ readKey () { return decoding.readVarString(this.restDecoder) } } export class DSDecoderV2 { /** * @param {decoding.Decoder} decoder */ constructor (decoder) { /** * @private */ this.dsCurrVal = 0 this.restDecoder = decoder } resetDsCurVal () { this.dsCurrVal = 0 } /** * @return {number} */ readDsClock () { this.dsCurrVal += decoding.readVarUint(this.restDecoder) return this.dsCurrVal } /** * @return {number} */ readDsLen () { const diff = decoding.readVarUint(this.restDecoder) + 1 this.dsCurrVal += diff return diff } } export class UpdateDecoderV2 extends DSDecoderV2 { /** * @param {decoding.Decoder} decoder */ constructor (decoder) { super(decoder) /** * List of cached keys. If the keys[id] does not exist, we read a new key * from stringEncoder and push it to keys. * * @type {Array} */ this.keys = [] decoding.readVarUint(decoder) // read feature flag - currently unused this.keyClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) this.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder)) this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) this.rightClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder)) this.infoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8) this.stringDecoder = new decoding.StringDecoder(decoding.readVarUint8Array(decoder)) this.parentInfoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8) this.typeRefDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder)) this.lenDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder)) } /** * @return {ID} */ readLeftID () { return new ID(this.clientDecoder.read(), this.leftClockDecoder.read()) } /** * @return {ID} */ readRightID () { return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()) } /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. */ readClient () { return this.clientDecoder.read() } /** * @return {number} info An unsigned 8-bit integer */ readInfo () { return /** @type {number} */ (this.infoDecoder.read()) } /** * @return {string} */ readString () { return this.stringDecoder.read() } /** * @return {boolean} */ readParentInfo () { return this.parentInfoDecoder.read() === 1 } /** * @return {number} An unsigned 8-bit integer */ readTypeRef () { return this.typeRefDecoder.read() } /** * Write len of a struct - well suited for Opt RLE encoder. * * @return {number} */ readLen () { return this.lenDecoder.read() } /** * @return {any} */ readAny () { return decoding.readAny(this.restDecoder) } /** * @return {Uint8Array} */ readBuf () { return decoding.readVarUint8Array(this.restDecoder) } /** * This is mainly here for legacy purposes. * * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. * * @return {any} */ readJSON () { return decoding.readAny(this.restDecoder) } /** * @return {string} */ readKey () { const keyClock = this.keyClockDecoder.read() if (keyClock < this.keys.length) { return this.keys[keyClock] } else { const key = this.stringDecoder.read() this.keys.push(key) return key } } } yjs-13.6.8/src/utils/UpdateEncoder.js000066400000000000000000000171151450200430400174220ustar00rootroot00000000000000 import * as error from 'lib0/error' import * as encoding from 'lib0/encoding' import { ID // eslint-disable-line } from '../internals.js' export class DSEncoderV1 { constructor () { this.restEncoder = encoding.createEncoder() } toUint8Array () { return encoding.toUint8Array(this.restEncoder) } resetDsCurVal () { // nop } /** * @param {number} clock */ writeDsClock (clock) { encoding.writeVarUint(this.restEncoder, clock) } /** * @param {number} len */ writeDsLen (len) { encoding.writeVarUint(this.restEncoder, len) } } export class UpdateEncoderV1 extends DSEncoderV1 { /** * @param {ID} id */ writeLeftID (id) { encoding.writeVarUint(this.restEncoder, id.client) encoding.writeVarUint(this.restEncoder, id.clock) } /** * @param {ID} id */ writeRightID (id) { encoding.writeVarUint(this.restEncoder, id.client) encoding.writeVarUint(this.restEncoder, id.clock) } /** * Use writeClient and writeClock instead of writeID if possible. * @param {number} client */ writeClient (client) { encoding.writeVarUint(this.restEncoder, client) } /** * @param {number} info An unsigned 8-bit integer */ writeInfo (info) { encoding.writeUint8(this.restEncoder, info) } /** * @param {string} s */ writeString (s) { encoding.writeVarString(this.restEncoder, s) } /** * @param {boolean} isYKey */ writeParentInfo (isYKey) { encoding.writeVarUint(this.restEncoder, isYKey ? 1 : 0) } /** * @param {number} info An unsigned 8-bit integer */ writeTypeRef (info) { encoding.writeVarUint(this.restEncoder, info) } /** * Write len of a struct - well suited for Opt RLE encoder. * * @param {number} len */ writeLen (len) { encoding.writeVarUint(this.restEncoder, len) } /** * @param {any} any */ writeAny (any) { encoding.writeAny(this.restEncoder, any) } /** * @param {Uint8Array} buf */ writeBuf (buf) { encoding.writeVarUint8Array(this.restEncoder, buf) } /** * @param {any} embed */ writeJSON (embed) { encoding.writeVarString(this.restEncoder, JSON.stringify(embed)) } /** * @param {string} key */ writeKey (key) { encoding.writeVarString(this.restEncoder, key) } } export class DSEncoderV2 { constructor () { this.restEncoder = encoding.createEncoder() // encodes all the rest / non-optimized this.dsCurrVal = 0 } toUint8Array () { return encoding.toUint8Array(this.restEncoder) } resetDsCurVal () { this.dsCurrVal = 0 } /** * @param {number} clock */ writeDsClock (clock) { const diff = clock - this.dsCurrVal this.dsCurrVal = clock encoding.writeVarUint(this.restEncoder, diff) } /** * @param {number} len */ writeDsLen (len) { if (len === 0) { error.unexpectedCase() } encoding.writeVarUint(this.restEncoder, len - 1) this.dsCurrVal += len } } export class UpdateEncoderV2 extends DSEncoderV2 { constructor () { super() /** * @type {Map} */ this.keyMap = new Map() /** * Refers to the next uniqe key-identifier to me used. * See writeKey method for more information. * * @type {number} */ this.keyClock = 0 this.keyClockEncoder = new encoding.IntDiffOptRleEncoder() this.clientEncoder = new encoding.UintOptRleEncoder() this.leftClockEncoder = new encoding.IntDiffOptRleEncoder() this.rightClockEncoder = new encoding.IntDiffOptRleEncoder() this.infoEncoder = new encoding.RleEncoder(encoding.writeUint8) this.stringEncoder = new encoding.StringEncoder() this.parentInfoEncoder = new encoding.RleEncoder(encoding.writeUint8) this.typeRefEncoder = new encoding.UintOptRleEncoder() this.lenEncoder = new encoding.UintOptRleEncoder() } toUint8Array () { const encoder = encoding.createEncoder() encoding.writeVarUint(encoder, 0) // this is a feature flag that we might use in the future encoding.writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.infoEncoder)) encoding.writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.parentInfoEncoder)) encoding.writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array()) encoding.writeVarUint8Array(encoder, this.lenEncoder.toUint8Array()) // @note The rest encoder is appended! (note the missing var) encoding.writeUint8Array(encoder, encoding.toUint8Array(this.restEncoder)) return encoding.toUint8Array(encoder) } /** * @param {ID} id */ writeLeftID (id) { this.clientEncoder.write(id.client) this.leftClockEncoder.write(id.clock) } /** * @param {ID} id */ writeRightID (id) { this.clientEncoder.write(id.client) this.rightClockEncoder.write(id.clock) } /** * @param {number} client */ writeClient (client) { this.clientEncoder.write(client) } /** * @param {number} info An unsigned 8-bit integer */ writeInfo (info) { this.infoEncoder.write(info) } /** * @param {string} s */ writeString (s) { this.stringEncoder.write(s) } /** * @param {boolean} isYKey */ writeParentInfo (isYKey) { this.parentInfoEncoder.write(isYKey ? 1 : 0) } /** * @param {number} info An unsigned 8-bit integer */ writeTypeRef (info) { this.typeRefEncoder.write(info) } /** * Write len of a struct - well suited for Opt RLE encoder. * * @param {number} len */ writeLen (len) { this.lenEncoder.write(len) } /** * @param {any} any */ writeAny (any) { encoding.writeAny(this.restEncoder, any) } /** * @param {Uint8Array} buf */ writeBuf (buf) { encoding.writeVarUint8Array(this.restEncoder, buf) } /** * This is mainly here for legacy purposes. * * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. * * @param {any} embed */ writeJSON (embed) { encoding.writeAny(this.restEncoder, embed) } /** * Property keys are often reused. For example, in y-prosemirror the key `bold` might * occur very often. For a 3d application, the key `position` might occur very often. * * We cache these keys in a Map and refer to them via a unique number. * * @param {string} key */ writeKey (key) { const clock = this.keyMap.get(key) if (clock === undefined) { /** * @todo uncomment to introduce this feature finally * * Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString. * Furthermore, I forgot to set the keyclock. So everything was working fine. * * However, this feature here is basically useless as it is not being used (it actually only consumes extra memory). * * I don't know yet how to reintroduce this feature.. * * Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag. * */ // this.keyMap.set(key, this.keyClock) this.keyClockEncoder.write(this.keyClock++) this.stringEncoder.write(key) } else { this.keyClockEncoder.write(clock) } } } yjs-13.6.8/src/utils/YEvent.js000066400000000000000000000205501450200430400161070ustar00rootroot00000000000000 import { isDeleted, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line } from '../internals.js' import * as set from 'lib0/set' import * as array from 'lib0/array' import * as error from 'lib0/error' const errorComputeChanges = 'You must not compute changes after the event-handler fired.' /** * @template {AbstractType} T * YEvent describes the changes on a YType. */ export class YEvent { /** * @param {T} target The changed type. * @param {Transaction} transaction */ constructor (target, transaction) { /** * The type on which this event was created on. * @type {T} */ this.target = target /** * The current target on which the observe callback is called. * @type {AbstractType} */ this.currentTarget = target /** * The transaction that triggered this event. * @type {Transaction} */ this.transaction = transaction /** * @type {Object|null} */ this._changes = null /** * @type {null | Map} */ this._keys = null /** * @type {null | Array<{ insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object }>} */ this._delta = null /** * @type {Array|null} */ this._path = null } /** * Computes the path from `y` to the changed type. * * @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with. * * The following property holds: * @example * let type = y * event.path.forEach(dir => { * type = type.get(dir) * }) * type === event.target // => true */ get path () { return this._path || (this._path = getPathTo(this.currentTarget, this.target)) } /** * Check if a struct is deleted by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ deletes (struct) { return isDeleted(this.transaction.deleteSet, struct.id) } /** * @type {Map} */ get keys () { if (this._keys === null) { if (this.transaction.doc._transactionCleanups.length === 0) { throw error.create(errorComputeChanges) } const keys = new Map() const target = this.target const changed = /** @type Set */ (this.transaction.changed.get(target)) changed.forEach(key => { if (key !== null) { const item = /** @type {Item} */ (target._map.get(key)) /** * @type {'delete' | 'add' | 'update'} */ let action let oldValue if (this.adds(item)) { let prev = item.left while (prev !== null && this.adds(prev)) { prev = prev.left } if (this.deletes(item)) { if (prev !== null && this.deletes(prev)) { action = 'delete' oldValue = array.last(prev.content.getContent()) } else { return } } else { if (prev !== null && this.deletes(prev)) { action = 'update' oldValue = array.last(prev.content.getContent()) } else { action = 'add' oldValue = undefined } } } else { if (this.deletes(item)) { action = 'delete' oldValue = array.last(/** @type {Item} */ item.content.getContent()) } else { return // nop } } keys.set(key, { action, oldValue }) } }) this._keys = keys } return this._keys } /** * This is a computed property. Note that this can only be safely computed during the * event call. Computing this property after other changes happened might result in * unexpected behavior (incorrect computation of deltas). A safe way to collect changes * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} */ get delta () { return this.changes.delta } /** * Check if a struct is added by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ adds (struct) { return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0) } /** * This is a computed property. Note that this can only be safely computed during the * event call. Computing this property after other changes happened might result in * unexpected behavior (incorrect computation of deltas). A safe way to collect changes * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} */ get changes () { let changes = this._changes if (changes === null) { if (this.transaction.doc._transactionCleanups.length === 0) { throw error.create(errorComputeChanges) } const target = this.target const added = set.create() const deleted = set.create() /** * @type {Array<{insert:Array}|{delete:number}|{retain:number}>} */ const delta = [] changes = { added, deleted, delta, keys: this.keys } const changed = /** @type Set */ (this.transaction.changed.get(target)) if (changed.has(null)) { /** * @type {any} */ let lastOp = null const packOp = () => { if (lastOp) { delta.push(lastOp) } } for (let item = target._start; item !== null; item = item.right) { if (item.deleted) { if (this.deletes(item) && !this.adds(item)) { if (lastOp === null || lastOp.delete === undefined) { packOp() lastOp = { delete: 0 } } lastOp.delete += item.length deleted.add(item) } // else nop } else { if (this.adds(item)) { if (lastOp === null || lastOp.insert === undefined) { packOp() lastOp = { insert: [] } } lastOp.insert = lastOp.insert.concat(item.content.getContent()) added.add(item) } else { if (lastOp === null || lastOp.retain === undefined) { packOp() lastOp = { retain: 0 } } lastOp.retain += item.length } } } if (lastOp !== null && lastOp.retain === undefined) { packOp() } } this._changes = changes } return /** @type {any} */ (changes) } } /** * Compute the path from this type to the specified target. * * @example * // `child` should be accessible via `type.get(path[0]).get(path[1])..` * const path = type.getPathTo(child) * // assuming `type instanceof YArray` * console.log(path) // might look like => [2, 'key1'] * child === type.get(path[0]).get(path[1]) * * @param {AbstractType} parent * @param {AbstractType} child target * @return {Array} Path to the target * * @private * @function */ const getPathTo = (parent, child) => { const path = [] while (child._item !== null && child !== parent) { if (child._item.parentSub !== null) { // parent is map-ish path.unshift(child._item.parentSub) } else { // parent is array-ish let i = 0 let c = /** @type {AbstractType} */ (child._item.parent)._start while (c !== child._item && c !== null) { if (!c.deleted) { i++ } c = c.right } path.unshift(i) } child = /** @type {AbstractType} */ (child._item.parent) } return path } yjs-13.6.8/src/utils/encoding.js000066400000000000000000000572471450200430400165000ustar00rootroot00000000000000 /** * @module encoding */ /* * We use the first five bits in the info flag for determining the type of the struct. * * 0: GC * 1: Item with Deleted content * 2: Item with JSON content * 3: Item with Binary content * 4: Item with String content * 5: Item with Embed content (for richtext content) * 6: Item with Format content (a formatting marker for richtext content) * 7: Item with Type */ import { findIndexSS, getState, createID, getStateVector, readAndApplyDeleteSet, writeDeleteSet, createDeleteSetFromStructStore, transact, readItemContent, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, DSEncoderV2, DSDecoderV1, DSEncoderV1, mergeUpdates, mergeUpdatesV2, Skip, diffUpdateV2, convertUpdateFormatV2ToV1, DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line } from '../internals.js' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' import * as binary from 'lib0/binary' import * as map from 'lib0/map' import * as math from 'lib0/math' import * as array from 'lib0/array' /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Array} structs All structs by `client` * @param {number} client * @param {number} clock write structs starting with `ID(client,clock)` * * @function */ const writeStructs = (encoder, structs, client, clock) => { // write first id clock = math.max(clock, structs[0].id.clock) // make sure the first id exists const startNewStructs = findIndexSS(structs, clock) // write # encoded structs encoding.writeVarUint(encoder.restEncoder, structs.length - startNewStructs) encoder.writeClient(client) encoding.writeVarUint(encoder.restEncoder, clock) const firstStruct = structs[startNewStructs] // write first struct with an offset firstStruct.write(encoder, clock - firstStruct.id.clock) for (let i = startNewStructs + 1; i < structs.length; i++) { structs[i].write(encoder, 0) } } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {StructStore} store * @param {Map} _sm * * @private * @function */ export const writeClientsStructs = (encoder, store, _sm) => { // we filter all valid _sm entries into sm const sm = new Map() _sm.forEach((clock, client) => { // only write if new structs are available if (getState(store, client) > clock) { sm.set(client, clock) } }) getStateVector(store).forEach((_clock, client) => { if (!_sm.has(client)) { sm.set(client, 0) } }) // write # states that were updated encoding.writeVarUint(encoder.restEncoder, sm.size) // Write items with higher client ids first // This heavily improves the conflict algorithm. array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { writeStructs(encoder, /** @type {Array} */ (store.clients.get(client)), client, clock) }) } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from. * @param {Doc} doc * @return {Map }>} * * @private * @function */ export const readClientsStructRefs = (decoder, doc) => { /** * @type {Map }>} */ const clientRefs = map.create() const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numOfStateUpdates; i++) { const numberOfStructs = decoding.readVarUint(decoder.restDecoder) /** * @type {Array} */ const refs = new Array(numberOfStructs) const client = decoder.readClient() let clock = decoding.readVarUint(decoder.restDecoder) // const start = performance.now() clientRefs.set(client, { i: 0, refs }) for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo() switch (binary.BITS5 & info) { case 0: { // GC const len = decoder.readLen() refs[i] = new GC(createID(client, clock), len) clock += len break } case 10: { // Skip Struct (nothing to apply) // @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing. const len = decoding.readVarUint(decoder.restDecoder) refs[i] = new Skip(createID(client, clock), len) clock += len break } default: { // Item with content /** * The optimized implementation doesn't use any variables because inlining variables is faster. * Below a non-optimized version is shown that implements the basic algorithm with * a few comments */ const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const struct = new Item( createID(client, clock), null, // leftd (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin null, // right (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info) // item content ) /* A non-optimized implementation of the above algorithm: // The item that was originally to the left of this item. const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null // The item that was originally to the right of this item. const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null const struct = new Item( createID(client, clock), null, // leftd origin, // origin null, // right rightOrigin, // right origin cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info) // item content ) */ refs[i] = struct clock += struct.length } } } // console.log('time to read: ', performance.now() - start) // @todo remove } return clientRefs } /** * Resume computing structs generated by struct readers. * * While there is something to do, we integrate structs in this order * 1. top element on stack, if stack is not empty * 2. next element from current struct reader (if empty, use next struct reader) * * If struct causally depends on another struct (ref.missing), we put next reader of * `ref.id.client` on top of stack. * * At some point we find a struct that has no causal dependencies, * then we start emptying the stack. * * It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2) * depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`. * * This method is implemented in a way so that we can resume computation if this update * causally depends on another update. * * @param {Transaction} transaction * @param {StructStore} store * @param {Map} clientsStructRefs * @return { null | { update: Uint8Array, missing: Map } } * * @private * @function */ const integrateStructs = (transaction, store, clientsStructRefs) => { /** * @type {Array} */ const stack = [] // sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user. let clientsStructRefsIds = array.from(clientsStructRefs.keys()).sort((a, b) => a - b) if (clientsStructRefsIds.length === 0) { return null } const getNextStructTarget = () => { if (clientsStructRefsIds.length === 0) { return null } let nextStructsTarget = /** @type {{i:number,refs:Array}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1])) while (nextStructsTarget.refs.length === nextStructsTarget.i) { clientsStructRefsIds.pop() if (clientsStructRefsIds.length > 0) { nextStructsTarget = /** @type {{i:number,refs:Array}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1])) } else { return null } } return nextStructsTarget } let curStructsTarget = getNextStructTarget() if (curStructsTarget === null && stack.length === 0) { return null } /** * @type {StructStore} */ const restStructs = new StructStore() const missingSV = new Map() /** * @param {number} client * @param {number} clock */ const updateMissingSv = (client, clock) => { const mclock = missingSV.get(client) if (mclock == null || mclock > clock) { missingSV.set(client, clock) } } /** * @type {GC|Item} */ let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++] // caching the state because it is used very often const state = new Map() const addStackToRestSS = () => { for (const item of stack) { const client = item.id.client const unapplicableItems = clientsStructRefs.get(client) if (unapplicableItems) { // decrement because we weren't able to apply previous operation unapplicableItems.i-- restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i)) clientsStructRefs.delete(client) unapplicableItems.i = 0 unapplicableItems.refs = [] } else { // item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue restStructs.clients.set(client, [item]) } // remove client from clientsStructRefsIds to prevent users from applying the same update again clientsStructRefsIds = clientsStructRefsIds.filter(c => c !== client) } stack.length = 0 } // iterate over all struct readers until we are done while (true) { if (stackHead.constructor !== Skip) { const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client)) const offset = localClock - stackHead.id.clock if (offset < 0) { // update from the same client is missing stack.push(stackHead) updateMissingSv(stackHead.id.client, stackHead.id.clock - 1) // hid a dead wall, add all items from stack to restSS addStackToRestSS() } else { const missing = stackHead.getMissing(transaction, store) if (missing !== null) { stack.push(stackHead) // get the struct reader that has the missing struct /** * @type {{ refs: Array, i: number }} */ const structRefs = clientsStructRefs.get(/** @type {number} */ (missing)) || { refs: [], i: 0 } if (structRefs.refs.length === structRefs.i) { // This update message causally depends on another update message that doesn't exist yet updateMissingSv(/** @type {number} */ (missing), getState(store, missing)) addStackToRestSS() } else { stackHead = structRefs.refs[structRefs.i++] continue } } else if (offset === 0 || offset < stackHead.length) { // all fine, apply the stackhead stackHead.integrate(transaction, offset) state.set(stackHead.id.client, stackHead.id.clock + stackHead.length) } } } // iterate to next stackHead if (stack.length > 0) { stackHead = /** @type {GC|Item} */ (stack.pop()) } else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) { stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]) } else { curStructsTarget = getNextStructTarget() if (curStructsTarget === null) { // we are done! break } else { stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]) } } } if (restStructs.clients.size > 0) { const encoder = new UpdateEncoderV2() writeClientsStructs(encoder, restStructs, new Map()) // write empty deleteset // writeDeleteSet(encoder, new DeleteSet()) encoding.writeVarUint(encoder.restEncoder, 0) // => no need for an extra function call, just write 0 deletes return { missing: missingSV, update: encoder.toUint8Array() } } return null } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Transaction} transaction * * @private * @function */ export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState) /** * Read and apply a document update. * * This function has the same effect as `applyUpdate` but accepts an decoder. * * @param {decoding.Decoder} decoder * @param {Doc} ydoc * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder] * * @function */ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact(ydoc, transaction => { // force that transaction.local is set to non-local transaction.local = false let retry = false const doc = transaction.doc const store = doc.store // let start = performance.now() const ss = readClientsStructRefs(structDecoder, doc) // console.log('time to read structs: ', performance.now() - start) // @todo remove // start = performance.now() // console.log('time to merge: ', performance.now() - start) // @todo remove // start = performance.now() const restStructs = integrateStructs(transaction, store, ss) const pending = store.pendingStructs if (pending) { // check if we can apply something for (const [client, clock] of pending.missing) { if (clock < getState(store, client)) { retry = true break } } if (restStructs) { // merge restStructs into store.pending for (const [client, clock] of restStructs.missing) { const mclock = pending.missing.get(client) if (mclock == null || mclock > clock) { pending.missing.set(client, clock) } } pending.update = mergeUpdatesV2([pending.update, restStructs.update]) } } else { store.pendingStructs = restStructs } // console.log('time to integrate: ', performance.now() - start) // @todo remove // start = performance.now() const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store) if (store.pendingDs) { // @todo we could make a lower-bound state-vector check as we do above const pendingDSUpdate = new UpdateDecoderV2(decoding.createDecoder(store.pendingDs)) decoding.readVarUint(pendingDSUpdate.restDecoder) // read 0 structs, because we only encode deletes in pendingdsupdate const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store) if (dsRest && dsRest2) { // case 1: ds1 != null && ds2 != null store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]) } else { // case 2: ds1 != null // case 3: ds2 != null // case 4: ds1 == null && ds2 == null store.pendingDs = dsRest || dsRest2 } } else { // Either dsRest == null && pendingDs == null OR dsRest != null store.pendingDs = dsRest } // console.log('time to cleanup: ', performance.now() - start) // @todo remove // start = performance.now() // console.log('time to resume delete readers: ', performance.now() - start) // @todo remove // start = performance.now() if (retry) { const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update store.pendingStructs = null applyUpdateV2(transaction.doc, update) } }, transactionOrigin, false) /** * Read and apply a document update. * * This function has the same effect as `applyUpdate` but accepts an decoder. * * @param {decoding.Decoder} decoder * @param {Doc} ydoc * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * * @function */ export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new UpdateDecoderV1(decoder)) /** * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * * This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder. * * @param {Doc} ydoc * @param {Uint8Array} update * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * * @function */ export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => { const decoder = decoding.createDecoder(update) readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder)) } /** * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * * This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder. * * @param {Doc} ydoc * @param {Uint8Array} update * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * * @function */ export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1) /** * Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will * only write the operations that are missing. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Doc} doc * @param {Map} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs * * @function */ export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => { writeClientsStructs(encoder, doc.store, targetStateVector) writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store)) } /** * Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will * only write the operations that are missing. * * Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder * * @param {Doc} doc * @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs * @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder] * @return {Uint8Array} * * @function */ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => { const targetStateVector = decodeStateVector(encodedTargetStateVector) writeStateAsUpdate(encoder, doc, targetStateVector) const updates = [encoder.toUint8Array()] // also add the pending updates (if there are any) if (doc.store.pendingDs) { updates.push(doc.store.pendingDs) } if (doc.store.pendingStructs) { updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector)) } if (updates.length > 1) { if (encoder.constructor === UpdateEncoderV1) { return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update))) } else if (encoder.constructor === UpdateEncoderV2) { return mergeUpdatesV2(updates) } } return updates[0] } /** * Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will * only write the operations that are missing. * * Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder * * @param {Doc} doc * @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs * @return {Uint8Array} * * @function */ export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1()) /** * Read state vector from Decoder and return as Map * * @param {DSDecoderV1 | DSDecoderV2} decoder * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ export const readStateVector = decoder => { const ss = new Map() const ssLength = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < ssLength; i++) { const client = decoding.readVarUint(decoder.restDecoder) const clock = decoding.readVarUint(decoder.restDecoder) ss.set(client, clock) } return ss } /** * Read decodedState and return State as Map. * * @param {Uint8Array} decodedState * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ // export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState))) /** * Read decodedState and return State as Map. * * @param {Uint8Array} decodedState * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1(decoding.createDecoder(decodedState))) /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {Map} sv * @function */ export const writeStateVector = (encoder, sv) => { encoding.writeVarUint(encoder.restEncoder, sv.size) array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => { encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping encoding.writeVarUint(encoder.restEncoder, clock) }) return encoder } /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {Doc} doc * * @function */ export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store)) /** * Encode State as Uint8Array. * * @param {Doc|Map} doc * @param {DSEncoderV1 | DSEncoderV2} [encoder] * @return {Uint8Array} * * @function */ export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => { if (doc instanceof Map) { writeStateVector(encoder, doc) } else { writeDocumentStateVector(encoder, doc) } return encoder.toUint8Array() } /** * Encode State as Uint8Array. * * @param {Doc|Map} doc * @return {Uint8Array} * * @function */ export const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1()) yjs-13.6.8/src/utils/isParentOf.js000066400000000000000000000007751450200430400167560ustar00rootroot00000000000000 import { AbstractType, Item } from '../internals.js' // eslint-disable-line /** * Check if `parent` is a parent of `child`. * * @param {AbstractType} parent * @param {Item|null} child * @return {Boolean} Whether `parent` is a parent of `child`. * * @private * @function */ export const isParentOf = (parent, child) => { while (child !== null) { if (child.parent === parent) { return true } child = /** @type {AbstractType} */ (child.parent)._item } return false } yjs-13.6.8/src/utils/logging.js000066400000000000000000000007351450200430400163260ustar00rootroot00000000000000 import { AbstractType // eslint-disable-line } from '../internals.js' /** * Convenient helper to log type information. * * Do not use in productive systems as the output can be immense! * * @param {AbstractType} type */ export const logType = type => { const res = [] let n = type._start while (n) { res.push(n) n = n.right } console.log('Children: ', res) console.log('Children content: ', res.filter(m => !m.deleted).map(m => m.content)) } yjs-13.6.8/src/utils/updates.js000066400000000000000000000610301450200430400163400ustar00rootroot00000000000000 import * as binary from 'lib0/binary' import * as decoding from 'lib0/decoding' import * as encoding from 'lib0/encoding' import * as error from 'lib0/error' import * as f from 'lib0/function' import * as logging from 'lib0/logging' import * as map from 'lib0/map' import * as math from 'lib0/math' import * as string from 'lib0/string' import { ContentAny, ContentBinary, ContentDeleted, ContentDoc, ContentEmbed, ContentFormat, ContentJSON, ContentString, ContentType, createID, decodeStateVector, DSEncoderV1, DSEncoderV2, GC, Item, mergeDeleteSets, readDeleteSet, readItemContent, Skip, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, writeDeleteSet, YXmlElement, YXmlHook } from '../internals.js' /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder */ function * lazyStructReaderGenerator (decoder) { const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numOfStateUpdates; i++) { const numberOfStructs = decoding.readVarUint(decoder.restDecoder) const client = decoder.readClient() let clock = decoding.readVarUint(decoder.restDecoder) for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo() // @todo use switch instead of ifs if (info === 10) { const len = decoding.readVarUint(decoder.restDecoder) yield new Skip(createID(client, clock), len) clock += len } else if ((binary.BITS5 & info) !== 0) { const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const struct = new Item( createID(client, clock), null, // left (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin null, // right (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin // @ts-ignore Force writing a string here. cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info) // item content ) yield struct clock += struct.length } else { const len = decoder.readLen() yield new GC(createID(client, clock), len) clock += len } } } } export class LazyStructReader { /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {boolean} filterSkips */ constructor (decoder, filterSkips) { this.gen = lazyStructReaderGenerator(decoder) /** * @type {null | Item | Skip | GC} */ this.curr = null this.done = false this.filterSkips = filterSkips this.next() } /** * @return {Item | GC | Skip |null} */ next () { // ignore "Skip" structs do { this.curr = this.gen.next().value || null } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip) return this.curr } } /** * @param {Uint8Array} update * */ export const logUpdate = update => logUpdateV2(update, UpdateDecoderV1) /** * @param {Uint8Array} update * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder] * */ export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => { const structs = [] const updateDecoder = new YDecoder(decoding.createDecoder(update)) const lazyDecoder = new LazyStructReader(updateDecoder, false) for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { structs.push(curr) } logging.print('Structs: ', structs) const ds = readDeleteSet(updateDecoder) logging.print('DeleteSet: ', ds) } /** * @param {Uint8Array} update * */ export const decodeUpdate = (update) => decodeUpdateV2(update, UpdateDecoderV1) /** * @param {Uint8Array} update * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder] * */ export const decodeUpdateV2 = (update, YDecoder = UpdateDecoderV2) => { const structs = [] const updateDecoder = new YDecoder(decoding.createDecoder(update)) const lazyDecoder = new LazyStructReader(updateDecoder, false) for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { structs.push(curr) } return { structs, ds: readDeleteSet(updateDecoder) } } export class LazyStructWriter { /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ constructor (encoder) { this.currClient = 0 this.startClock = 0 this.written = 0 this.encoder = encoder /** * We want to write operations lazily, but also we need to know beforehand how many operations we want to write for each client. * * This kind of meta-information (#clients, #structs-per-client-written) is written to the restEncoder. * * We fragment the restEncoder and store a slice of it per-client until we know how many clients there are. * When we flush (toUint8Array) we write the restEncoder using the fragments and the meta-information. * * @type {Array<{ written: number, restEncoder: Uint8Array }>} */ this.clientStructs = [] } } /** * @param {Array} updates * @return {Uint8Array} */ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1) /** * @param {Uint8Array} update * @param {typeof DSEncoderV1 | typeof DSEncoderV2} YEncoder * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder * @return {Uint8Array} */ export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => { const encoder = new YEncoder() const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false) let curr = updateDecoder.curr if (curr !== null) { let size = 0 let currClient = curr.id.client let stopCounting = curr.id.clock !== 0 // must start at 0 let currClock = stopCounting ? 0 : curr.id.clock + curr.length for (; curr !== null; curr = updateDecoder.next()) { if (currClient !== curr.id.client) { if (currClock !== 0) { size++ // We found a new client // write what we have to the encoder encoding.writeVarUint(encoder.restEncoder, currClient) encoding.writeVarUint(encoder.restEncoder, currClock) } currClient = curr.id.client currClock = 0 stopCounting = curr.id.clock !== 0 } // we ignore skips if (curr.constructor === Skip) { stopCounting = true } if (!stopCounting) { currClock = curr.id.clock + curr.length } } // write what we have if (currClock !== 0) { size++ encoding.writeVarUint(encoder.restEncoder, currClient) encoding.writeVarUint(encoder.restEncoder, currClock) } // prepend the size of the state vector const enc = encoding.createEncoder() encoding.writeVarUint(enc, size) encoding.writeBinaryEncoder(enc, encoder.restEncoder) encoder.restEncoder = enc return encoder.toUint8Array() } else { encoding.writeVarUint(encoder.restEncoder, 0) return encoder.toUint8Array() } } /** * @param {Uint8Array} update * @return {Uint8Array} */ export const encodeStateVectorFromUpdate = update => encodeStateVectorFromUpdateV2(update, DSEncoderV1, UpdateDecoderV1) /** * @param {Uint8Array} update * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder * @return {{ from: Map, to: Map }} */ export const parseUpdateMetaV2 = (update, YDecoder = UpdateDecoderV2) => { /** * @type {Map} */ const from = new Map() /** * @type {Map} */ const to = new Map() const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false) let curr = updateDecoder.curr if (curr !== null) { let currClient = curr.id.client let currClock = curr.id.clock // write the beginning to `from` from.set(currClient, currClock) for (; curr !== null; curr = updateDecoder.next()) { if (currClient !== curr.id.client) { // We found a new client // write the end to `to` to.set(currClient, currClock) // write the beginning to `from` from.set(curr.id.client, curr.id.clock) // update currClient currClient = curr.id.client } currClock = curr.id.clock + curr.length } // write the end to `to` to.set(currClient, currClock) } return { from, to } } /** * @param {Uint8Array} update * @return {{ from: Map, to: Map }} */ export const parseUpdateMeta = update => parseUpdateMetaV2(update, UpdateDecoderV1) /** * This method is intended to slice any kind of struct and retrieve the right part. * It does not handle side-effects, so it should only be used by the lazy-encoder. * * @param {Item | GC | Skip} left * @param {number} diff * @return {Item | GC} */ const sliceStruct = (left, diff) => { if (left.constructor === GC) { const { client, clock } = left.id return new GC(createID(client, clock + diff), left.length - diff) } else if (left.constructor === Skip) { const { client, clock } = left.id return new Skip(createID(client, clock + diff), left.length - diff) } else { const leftItem = /** @type {Item} */ (left) const { client, clock } = leftItem.id return new Item( createID(client, clock + diff), null, createID(client, clock + diff - 1), null, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, leftItem.content.splice(diff) ) } } /** * * This function works similarly to `readUpdateV2`. * * @param {Array} updates * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] * @return {Uint8Array} */ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { if (updates.length === 1) { return updates[0] } const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update))) let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true)) /** * @todo we don't need offset because we always slice before * @type {null | { struct: Item | GC | Skip, offset: number }} */ let currWrite = null const updateEncoder = new YEncoder() // write structs lazily const lazyStructEncoder = new LazyStructWriter(updateEncoder) // Note: We need to ensure that all lazyStructDecoders are fully consumed // Note: Should merge document updates whenever possible - even from different updates // Note: Should handle that some operations cannot be applied yet () while (true) { // Write higher clients first β‡’ sort by clientID & clock and remove decoders without content lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null) lazyStructDecoders.sort( /** @type {function(any,any):number} */ (dec1, dec2) => { if (dec1.curr.id.client === dec2.curr.id.client) { const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock if (clockDiff === 0) { // @todo remove references to skip since the structDecoders must filter Skips. return dec1.curr.constructor === dec2.curr.constructor ? 0 : dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway. } else { return clockDiff } } else { return dec2.curr.id.client - dec1.curr.id.client } } ) if (lazyStructDecoders.length === 0) { break } const currDecoder = lazyStructDecoders[0] // write from currDecoder until the next operation is from another client or if filler-struct // then we need to reorder the decoders and find the next operation to write const firstClient = /** @type {Item | GC} */ (currDecoder.curr).id.client if (currWrite !== null) { let curr = /** @type {Item | GC | null} */ (currDecoder.curr) let iterated = false // iterate until we find something that we haven't written already // remember: first the high client-ids are written while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { curr = currDecoder.next() iterated = true } if ( curr === null || // current decoder is empty curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient` (iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates ) { continue } if (firstClient !== currWrite.struct.id.client) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) currWrite = { struct: curr, offset: 0 } currDecoder.next() } else { if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { // @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock) if (currWrite.struct.constructor === Skip) { // extend existing skip currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock } else { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length /** * @type {Skip} */ const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff) currWrite = { struct, offset: 0 } } } else { // if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) { const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock if (diff > 0) { if (currWrite.struct.constructor === Skip) { // prefer to slice Skip because the other struct might contain more information currWrite.struct.length -= diff } else { curr = sliceStruct(curr, diff) } } if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) currWrite = { struct: curr, offset: 0 } currDecoder.next() } } } } else { currWrite = { struct: /** @type {Item | GC} */ (currDecoder.curr), offset: 0 } currDecoder.next() } for ( let next = currDecoder.curr; next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next() ) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) currWrite = { struct: next, offset: 0 } } } if (currWrite !== null) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) currWrite = null } finishLazyStructWriting(lazyStructEncoder) const dss = updateDecoders.map(decoder => readDeleteSet(decoder)) const ds = mergeDeleteSets(dss) writeDeleteSet(updateEncoder, ds) return updateEncoder.toUint8Array() } /** * @param {Uint8Array} update * @param {Uint8Array} sv * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] */ export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { const state = decodeStateVector(sv) const encoder = new YEncoder() const lazyStructWriter = new LazyStructWriter(encoder) const decoder = new YDecoder(decoding.createDecoder(update)) const reader = new LazyStructReader(decoder, false) while (reader.curr) { const curr = reader.curr const currClient = curr.id.client const svClock = state.get(currClient) || 0 if (reader.curr.constructor === Skip) { // the first written struct shouldn't be a skip reader.next() continue } if (curr.id.clock + curr.length > svClock) { writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0)) reader.next() while (reader.curr && reader.curr.id.client === currClient) { writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0) reader.next() } } else { // read until something new comes up while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) { reader.next() } } } finishLazyStructWriting(lazyStructWriter) // write ds const ds = readDeleteSet(decoder) writeDeleteSet(encoder, ds) return encoder.toUint8Array() } /** * @param {Uint8Array} update * @param {Uint8Array} sv */ export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1) /** * @param {LazyStructWriter} lazyWriter */ const flushLazyStructWriter = lazyWriter => { if (lazyWriter.written > 0) { lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: encoding.toUint8Array(lazyWriter.encoder.restEncoder) }) lazyWriter.encoder.restEncoder = encoding.createEncoder() lazyWriter.written = 0 } } /** * @param {LazyStructWriter} lazyWriter * @param {Item | GC} struct * @param {number} offset */ const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { // flush curr if we start another client if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { flushLazyStructWriter(lazyWriter) } if (lazyWriter.written === 0) { lazyWriter.currClient = struct.id.client // write next client lazyWriter.encoder.writeClient(struct.id.client) // write startClock encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset) } struct.write(lazyWriter.encoder, offset) lazyWriter.written++ } /** * Call this function when we collected all parts and want to * put all the parts together. After calling this method, * you can continue using the UpdateEncoder. * * @param {LazyStructWriter} lazyWriter */ const finishLazyStructWriting = (lazyWriter) => { flushLazyStructWriter(lazyWriter) // this is a fresh encoder because we called flushCurr const restEncoder = lazyWriter.encoder.restEncoder /** * Now we put all the fragments together. * This works similarly to `writeClientsStructs` */ // write # states that were updated - i.e. the clients encoding.writeVarUint(restEncoder, lazyWriter.clientStructs.length) for (let i = 0; i < lazyWriter.clientStructs.length; i++) { const partStructs = lazyWriter.clientStructs[i] /** * Works similarly to `writeStructs` */ // write # encoded structs encoding.writeVarUint(restEncoder, partStructs.written) // write the rest of the fragment encoding.writeUint8Array(restEncoder, partStructs.restEncoder) } } /** * @param {Uint8Array} update * @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder * @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder */ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => { const updateDecoder = new YDecoder(decoding.createDecoder(update)) const lazyDecoder = new LazyStructReader(updateDecoder, false) const updateEncoder = new YEncoder() const lazyWriter = new LazyStructWriter(updateEncoder) for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0) } finishLazyStructWriting(lazyWriter) const ds = readDeleteSet(updateDecoder) writeDeleteSet(updateEncoder, ds) return updateEncoder.toUint8Array() } /** * @typedef {Object} ObfuscatorOptions * @property {boolean} [ObfuscatorOptions.formatting=true] * @property {boolean} [ObfuscatorOptions.subdocs=true] * @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName */ /** * @param {ObfuscatorOptions} obfuscator */ const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => { let i = 0 const mapKeyCache = map.create() const nodeNameCache = map.create() const formattingKeyCache = map.create() const formattingValueCache = map.create() formattingValueCache.set(null, null) // end of a formatting range should always be the end of a formatting range /** * @param {Item|GC|Skip} block * @return {Item|GC|Skip} */ return block => { switch (block.constructor) { case GC: case Skip: return block case Item: { const item = /** @type {Item} */ (block) const content = item.content switch (content.constructor) { case ContentDeleted: break case ContentType: { if (yxml) { const type = /** @type {ContentType} */ (content).type if (type instanceof YXmlElement) { type.nodeName = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'node-' + i) } if (type instanceof YXmlHook) { type.hookName = map.setIfUndefined(nodeNameCache, type.hookName, () => 'hook-' + i) } } break } case ContentAny: { const c = /** @type {ContentAny} */ (content) c.arr = c.arr.map(() => i) break } case ContentBinary: { const c = /** @type {ContentBinary} */ (content) c.content = new Uint8Array([i]) break } case ContentDoc: { const c = /** @type {ContentDoc} */ (content) if (subdocs) { c.opts = {} c.doc.guid = i + '' } break } case ContentEmbed: { const c = /** @type {ContentEmbed} */ (content) c.embed = {} break } case ContentFormat: { const c = /** @type {ContentFormat} */ (content) if (formatting) { c.key = map.setIfUndefined(formattingKeyCache, c.key, () => i + '') c.value = map.setIfUndefined(formattingValueCache, c.value, () => ({ i })) } break } case ContentJSON: { const c = /** @type {ContentJSON} */ (content) c.arr = c.arr.map(() => i) break } case ContentString: { const c = /** @type {ContentString} */ (content) c.str = string.repeat((i % 10) + '', c.str.length) break } default: // unknown content type error.unexpectedCase() } if (item.parentSub) { item.parentSub = map.setIfUndefined(mapKeyCache, item.parentSub, () => i + '') } i++ return block } default: // unknown block-type error.unexpectedCase() } } } /** * This function obfuscates the content of a Yjs update. This is useful to share * buggy Yjs documents while significantly limiting the possibility that a * developer can on the user. Note that it might still be possible to deduce * some information by analyzing the "structure" of the document or by analyzing * the typing behavior using the CRDT-related metadata that is still kept fully * intact. * * @param {Uint8Array} update * @param {ObfuscatorOptions} [opts] */ export const obfuscateUpdate = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV1, UpdateEncoderV1) /** * @param {Uint8Array} update * @param {ObfuscatorOptions} [opts] */ export const obfuscateUpdateV2 = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV2, UpdateEncoderV2) /** * @param {Uint8Array} update */ export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f.id, UpdateDecoderV1, UpdateEncoderV2) /** * @param {Uint8Array} update */ export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1) yjs-13.6.8/test.html000066400000000000000000000002221450200430400142470ustar00rootroot00000000000000 Testing Yjs yjs-13.6.8/tests/000077500000000000000000000000001450200430400135505ustar00rootroot00000000000000yjs-13.6.8/tests/compatibility.tests.js000066400000000000000000000543251450200430400201310ustar00rootroot00000000000000 /** * Testing if encoding/decoding compatibility and integration compatiblity is given. * We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches. * * The v1 documents were generated with Yjs v13.2.0 based on the randomisized tests. */ import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as buffer from 'lib0/buffer' /** * @param {t.TestCase} tc */ export const testArrayCompatibilityV1 = tc => { const oldDoc = 'BV8EAAcBBWFycmF5AAgABAADfQF9An0DgQQDAYEEAAEABMEDAAQAAccEAAQFASEABAsIc29tZXByb3ACqAQNAX0syAQLBAUBfYoHwQQPBAUBwQQQBAUByAQRBAUBfYoHyAQQBBEBfY0HyAQTBBEBfY0HyAQUBBEBfY0HyAQVBBEBfY0HyAQQBBMBfY4HyAQXBBMBfY4HwQQYBBMBxwQXBBgACAAEGgR9AX0CfQN9BMEBAAEBAQADxwQLBA8BIQAEIwhzb21lcHJvcAKoBCUBfSzHBBkEEwEhAAQnCHNvbWVwcm9wAqgEKQF9LMcCAAMAASEABCsIc29tZXByb3ACqAQtAX0syAEBAQIBfZMHyAQvAQIBfZMHwQEGAQcBAAPBBDEBBwEABMcBGQEVAAgABDoEfQF9An0DfQTHAAgADgAIAAQ/BH0BfQJ9A30ExwQYBBkACAAERAR9AX0CfQN9BMcEIwQPASEABEkIc29tZXByb3ACqARLAX0swQAKAAkBxwEZBDoACAAETgR9AX0CfQN9BMcEEAQXAAgABFMEfQF9An0DfQTHAxsDHAAIAARYBH0BfQJ9A30ExwECAQ0BIQAEXQhzb21lcHJvcAKoBF8BfSzHAQQBBQAIAARhBH0BfQJ9A30ExwABAAYBIQAEZghzb21lcHJvcAKoBGgBfSzHAywDLQEhAARqCHNvbWVwcm9wAqgEbAF9LMcCCgMPASEABG4Ic29tZXByb3ACqARwAX0sxwMfAQABIQAEcghzb21lcHJvcAKoBHQBfSzHABcAGAEhAAR2CHNvbWVwcm9wAqgEeAF9LMcCEwMfAAgABHoEfQF9An0DfQTHARYBFwAIAAR/BH0BfQJ9A30ExwAIBD8BIQAEhAEIc29tZXByb3ACqASGAQF9LMcAGQAPAAgABIgBBH0BfQJ9A30ExwMBAScACAAEjQEEfQF9An0DfQTHAB4CDgEhAASSAQhzb21lcHJvcAKoBJQBAX0syAErAR4EfYQIfYQIfYQIfYQIxwB7AHwBIQAEmgEIc29tZXByb3ACqAScAQF9LMgBRgIrA32ICH2ICH2ICMgAEgAIAn2KCH2KCHADAAEBBWFycmF5AYcDAAEhAAMBCHNvbWVwcm9wAqgDAwF9LIEDAQEABIEDBQEABEECAAHIAw8CAAF9hwfIAxACAAF9hwfBAxECAAHHAAEAAgEhAAMTCHNvbWVwcm9wAqgDFQF9LIEEAAKIAxgBfYwHyAMPAxABfY8HwQMaAxAByAMbAxABfY8HyAMPAxoBfZAHyAMdAxoBfZAHxwACAw8BIQADHwhzb21lcHJvcAKoAyEBfSzHAxoDGwEhAAMjCHNvbWVwcm9wAqgDJQF9LMcCAAMAASEAAycIc29tZXByb3ACqAMpAX0swQMQAxEByAMrAxEBfZIHyAMsAxEBfZIHyAMtAxEBfZIHwQMYAxkBAATIAQYBBwF9lAfIAzQBBwF9lAfHAQcELwAIAAM2BH0BfQJ9A30EyAEBAR4CfZUHfZUHyAMsAy0DfZcHfZcHfZcHxwQTBBQBIQADQAhzb21lcHJvcAKoA0IBfSxIAAACfZgHfZgHyANFAAABfZgHxwEEAQUACAADRwR9AX0CfQN9BMgDQAQUAX2ZB8EDTAQUAscABgIXASEAA08Ic29tZXByb3ACqANRAX0syAM/Ay0BfZwHyAMfAQABfZ0HxwM2BC8ACAADVQR9AX0CfQN9BMcDRQNGASEAA1oIc29tZXByb3ACqANcAX0sxwMPAx0BIQADXghzb21lcHJvcAKoA2ABfSzIAQgBBgF9pAfIAQQDRwN9pwd9pwd9pwfIAA8AEAJ9rAd9rAfHAAAAAwAIAANoBH0BfQJ9A30EyAMQAysDfbIHfbIHfbIHxwQxAQcACAADcAR9AX0CfQN9BMcBAAQfASEAA3UIc29tZXByb3ACqAN3AX0syAM/A1MBfbUHyAN5A1MCfbUHfbUHyAMtAy4DfbcHfbcHfbcHyAACAhMCfbkHfbkHyAOAAQITAX25B8cBKwM7AAgAA4IBBH0BfQJ9A30ExwEZARUBIQADhwEIc29tZXByb3ACqAOJAQF9LMcCHAQLAAgAA4sBBH0BfQJ9A30EyAQZBCcBfbsHyAOQAQQnAn27B327B8cDkAEDkQEBIQADkwEIc29tZXByb3ACqAOVAQF9LMcDaAADAAgAA5cBBH0BfQJ9A30ExwN5A3oACAADnAEEfQF9An0DfQTHA4sBBAsACAADoQEEfQF9An0DfQTHA5MBA5EBASEAA6YBCHNvbWVwcm9wAqgDqAEBfSzHAAADaAAIAAOqAQR9AX0CfQN9BMgADgAZA328B328B328B8gECwQjBH2CCH2CCH2CCH2CCMcDLQN8ASEAA7YBCHNvbWVwcm9wAqgDuAEBfSzHBAoEAAAIAAO6AQR9AX0CfQN9BMgDgAEDgQECfYUIfYUIWgIAAQEFYXJyYXkBAARHAgAACAACBQR9AX0CfQN9BMECBQIAAQADwQIFAgoBAATBAAICBQEAA8cABgAHAAgAAhcEfQF9An0DfQTHAxkECwEhAAIcCHNvbWVwcm9wAqgCHgF9LMcABAAFASEAAiAIc29tZXByb3ACqAIiAX0syAAIAA4BfZYHyAMRAxIBfZoHxwMdAx4ACAACJgR9AX0CfQN9BMcEFgQRAAgAAisEfQF9An0DfQTHBAoEAAAIAAIwBH0BfQJ9A30EyAAOABkDfaAHfaAHfaAHxwEFACIACAACOAR9AX0CfQN9BMcDJwQrAAgAAj0EfQF9An0DfQTHAhcABwAIAAJCBH0BfQJ9A30EyAEABB8CfaUHfaUHxwQrAwABIQACSQhzb21lcHJvcAKoAksBfSzHBCcEEwAIAAJNBH0BfQJ9A30ExwMbAxwACAACUgR9AX0CfQN9BMcEJwJNASEAAlcIc29tZXByb3ACqAJZAX0sxwQvBDAACAACWwR9AX0CfQN9BMcCPQQrASEAAmAIc29tZXByb3ACqAJiAX0sxwAYAycBIQACZAhzb21lcHJvcAKoAmYBfSzIAQEBHgJ9swd9swfIAmQDJwN9tAd9tAd9tAfHAkkDAAAIAAJtBH0BfQJ9A30ExwJkAmoACAACcgR9AX0CfQN9BMcCJgMeAAgAAncEfQF9An0DfQTHAiUDEgEhAAJ8CHNvbWVwcm9wAqgCfgF9LMgBFwEYBH24B324B324B324B8cBAQJoASEAAoQBCHNvbWVwcm9wAqgChgEBfSzHAkkCbQAIAAKIAQR9AX0CfQN9BMcCSAQfASEAAo0BCHNvbWVwcm9wAqgCjwEBfSzIAQYEMQR9vgd9vgd9vgd9vgfHAAAAAwEhAAKVAQhzb21lcHJvcAKoApcBAX0sxwJNBBMBIQACmQEIc29tZXByb3ACqAKbAQF9LMcCJgJ3ASEAAp0BCHNvbWVwcm9wAqgCnwEBfSzHAAEABgAIAAKhAQR9AX0CfQN9BMgCjQEEHwF9gwjIAyMDGwF9hgjHBF0BDQAIAAKoAQR9AX0CfQN9BMcDPAEeAAgAAq0BBH0BfQJ9A30EagEAAQEFYXJyYXkByAEAAwABfYMHyAEBAwABfYMHwQECAwAByAEBAQIBfYYHyAEEAQIBfYYHyAEFAQIBfYYHwQEGAQIBxwEFAQYACAABCAR9AX0CfQN9BMEBAgEDAQAEwQEFAQgByAESAQgBfYsHyAETAQgBfYsHyAEUAQgBfYsHgQQAAYEBFgGIARcBfZEHxwEUARUACAABGQR9AX0CfQN9BMcBAQEEAAgAAR4EfQF9An0DfQTHARQBGQEhAAEjCHNvbWVwcm9wAqgBJQF9LMEDAQMFAQADxwEBAR4BIQABKwhzb21lcHJvcAKoAS0BfSzHAgUAHgEhAAEvCHNvbWVwcm9wAqgBMQF9LMcECwQjASEAATMIc29tZXByb3ACqAE1AX0sxwMtAy4ACAABNwR9AX0CfQN9BMcDDwMdAAgAATwEfQF9An0DfQTHAQIBDQAIAAFBBH0BfQJ9A30ExwQWBBEBIQABRghzb21lcHJvcAKoAUgBfSzBABgDJwHIAUoDJwF9nwfHBBcEGgAIAAFMBH0BfQJ9A30ExwEABB8BIQABUQhzb21lcHJvcAKoAVMBfSzIAx0DHgJ9oQd9oQfIARkBFQF9ogfIAhwECwN9qAd9qAd9qAfIAxEDEgF9qgfIBAABFgJ9qwd9qwfIABAAEQF9rQfIAV4AEQF9rQfIAV8AEQJ9rQd9rQfIAV4BXwR9rwd9rwd9rwd9rwfIABABXgN9sAd9sAd9sAfIAWgBXgF9sAfHBA8EEAAIAAFqBH0BfQJ9A30ExwQYBBkBIQABbwhzb21lcHJvcAKoAXEBfSzHAAcAEgEhAAFzCHNvbWVwcm9wAqgBdQF9LEcAAAAIAAF3BH0BfQJ9A30ExwMPATwBIQABfAhzb21lcHJvcAKoAX4BfSzIAXwBPAJ9ugd9ugfBAYEBATwCxwFoAWkACAABhAEEfQF9An0DfQTHAV8BYAAIAAGJAQR9AX0CfQN9BMcADgAZASEAAY4BCHNvbWVwcm9wAqgBkAEBfSzIAx8BAAF9vQfIAZIBAQABfb0HyAQVBBYCfb8Hfb8HxwQaBBgBIQABlgEIc29tZXByb3ACqAGYAQF9LMgBHgEEA32ACH2ACH2ACMcEGAFvAAgAAZ0BBH0BfQJ9A30ExwMTAAIBIQABogEIc29tZXByb3ACqAGkAQF9LMcBkgEBkwEBIQABpgEIc29tZXByb3ACqAGoAQF9LMcBnAEBBAEhAAGqAQhzb21lcHJvcAKoAawBAX0syAF8AYABBH2HCH2HCH2HCH2HCMgBpgEBkwEDfYkIfYkIfYkIYQAAAQEFYXJyYXkBiAAAAX2AB4EAAQHBAAAAAQLIAAQAAQF9gQfIAAEAAgF9hAfIAAYAAgF9hAfIAAcAAgF9hAfBAAgAAgHBAAgACQEAA8gACAAKAX2FB8EADgAKAcgADwAKAX2FB8gAEAAKAX2FB8cABwAIAAgAABIEfQF9An0DfQTIAgADAAF9iQfIABcDAAF9iQfHAA4ADwAIAAAZBH0BfQJ9A30ExwIFAgABIQAAHghzb21lcHJvcAKoACABfSzHAQUBEgEhAAAiCHNvbWVwcm9wAqgAJAF9LMcAHgIOAAgAACYEfQF9An0DfQTHBBQEFQAIAAArBH0BfQJ9A30ExwAAAAMACAAAMAR9AX0CfQN9BMcBBQAiAAgAADUEfQF9An0DfQTIAx4DGgN9mwd9mwd9mwfHAhcABwAIAAA9BH0BfQJ9A30ExwEYAxcBIQAAQghzb21lcHJvcAKoAEQBfSzBACIBEgEABMcDDwMdASEAAEsIc29tZXByb3ACqABNAX0sxwQYBBkBIQAATwhzb21lcHJvcAKoAFEBfSzHACIARgAIAABTBH0BfQJ9A30ExwMdAx4BIQAAWAhzb21lcHJvcAKoAFoBfSzIAB4AJgF9owfHAzYELwAIAABdBH0BfQJ9A30EyAQwAQIDfaYHfaYHfaYHyABkAQIBfakHyAAXABgCfa4Hfa4HxwQjBA8BIQAAaAhzb21lcHJvcAKoAGoBfSzHAycEKwAIAABsBH0BfQJ9A30ExwABAAYACAAAcQR9AX0CfQN9BMcAZABlAAgAAHYEfQF9An0DfQTIAAcAEgF9sQfIAHsAEgN9sQd9sQd9sQfIAA8AEAF9tgfHARMBFAAIAACAAQR9AX0CfQN9BMcDIwMbAAgAAIUBBH0BfQJ9A30ExwEVAQgACAAAigEEfQF9An0DfQTHAIoBAQgBIQAAjwEIc29tZXByb3ACqACRAQF9LMcCFwA9AAgAAJMBBH0BfQJ9A30ExwEYAEIACAAAmAEEfQF9An0DfQTHAzQDNQEhAACdAQhzb21lcHJvcAKoAJ8BAX0sxwAQABEBIQAAoQEIc29tZXByb3ACqACjAQF9LMgAgAEBFAF9gQjHBBYEEQEhAACmAQhzb21lcHJvcAKoAKgBAX0sxwAHAHsACAAAqgEEfQF9An0DfQQFABAAAQIDCQUPAR8CIwJDAkYFTAJQAlkCaQKQAQKeAQKiAQKnAQICDgAFCg0dAiECSgJYAmECZQJ9AoUBAo4BApYBApoBAp4BAgQUBAcMAhACGQEfBCQCKAIsAjEJSgJNAV4CZwJrAm8CcwJ3AoUBApMBApsBAgMWAAECAgULEgEUAhcCGwEgAiQCKAIrAS8FQQJNAlACWwJfAnYCiAEClAECpwECtwECARYAAQMBBwENBhYCJAInBCwCMAI0AkcCSgFSAnACdAJ9AoIBAo8BApcBAqMBAqcBAqsBAg==' const oldVal = JSON.parse('[[1,2,3,4],472,472,{"someprop":44},472,[1,2,3,4],{"someprop":44},[1,2,3,4],[1,2,3,4],[1,2,3,4],{"someprop":44},449,448,[1,2,3,4],[1,2,3,4],{"someprop":44},452,{"someprop":44},[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],452,[1,2,3,4],497,{"someprop":44},497,497,497,{"someprop":44},[1,2,3,4],522,522,452,470,{"someprop":44},[1,2,3,4],453,{"someprop":44},480,480,480,508,508,508,[1,2,3,4],[1,2,3,4],502,492,492,453,{"someprop":44},496,496,496,[1,2,3,4],496,493,495,495,495,495,493,[1,2,3,4],493,493,453,{"someprop":44},{"someprop":44},505,505,517,517,505,[1,2,3,4],{"someprop":44},509,{"someprop":44},521,521,521,509,477,{"someprop":44},{"someprop":44},485,485,{"someprop":44},515,{"someprop":44},451,{"someprop":44},[1,2,3,4],516,516,516,516,{"someprop":44},499,499,469,469,[1,2,3,4],[1,2,3,4],512,512,512,{"someprop":44},454,487,487,487,[1,2,3,4],[1,2,3,4],454,[1,2,3,4],[1,2,3,4],{"someprop":44},[1,2,3,4],459,[1,2,3,4],513,459,{"someprop":44},[1,2,3,4],482,{"someprop":44},[1,2,3,4],[1,2,3,4],459,[1,2,3,4],{"someprop":44},[1,2,3,4],484,454,510,510,510,510,468,{"someprop":44},468,[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],467,[1,2,3,4],467,486,486,486,[1,2,3,4],489,451,[1,2,3,4],{"someprop":44},[1,2,3,4],[1,2,3,4],{"someprop":44},{"someprop":44},483,[1,2,3,4],{"someprop":44},{"someprop":44},{"someprop":44},{"someprop":44},519,519,519,519,506,506,[1,2,3,4],{"someprop":44},464,{"someprop":44},481,481,[1,2,3,4],{"someprop":44},[1,2,3,4],464,475,475,475,463,{"someprop":44},[1,2,3,4],518,[1,2,3,4],[1,2,3,4],463,455,498,498,498,466,471,471,471,501,[1,2,3,4],501,501,476,{"someprop":44},466,[1,2,3,4],{"someprop":44},503,503,503,466,455,490,474,{"someprop":44},457,494,494,{"someprop":44},457,479,{"someprop":44},[1,2,3,4],500,500,500,{"someprop":44},[1,2,3,4],[1,2,3,4],{"someprop":44},{"someprop":44},{"someprop":44},[1,2,3,4],[1,2,3,4],{"someprop":44},[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3],491,491,[1,2,3,4],504,504,504,504,465,[1,2,3,4],{"someprop":44},460,{"someprop":44},488,488,488,[1,2,3,4],[1,2,3,4],{"someprop":44},{"someprop":44},514,514,514,514,{"someprop":44},{"someprop":44},{"someprop":44},458,[1,2,3,4],[1,2,3,4],462,[1,2,3,4],[1,2,3,4],{"someprop":44},462,{"someprop":44},[1,2,3,4],{"someprop":44},[1,2,3,4],507,{"someprop":44},{"someprop":44},507,507,{"someprop":44},{"someprop":44},[1,2,3,4],{"someprop":44},461,{"someprop":44},473,461,[1,2,3,4],461,511,511,461,{"someprop":44},{"someprop":44},520,520,520,[1,2,3,4],458]') const doc = new Y.Doc() Y.applyUpdate(doc, buffer.fromBase64(oldDoc)) t.compare(doc.getArray('array').toJSON(), oldVal) } /** * @param {t.TestCase} tc */ export const testMapDecodingCompatibilityV1 = tc => { const oldDoc = 'BVcEAKEBAAGhAwEBAAShBAABAAGhBAECoQEKAgAEoQQLAQAEoQMcAaEEFQGhAiECAAShAS4BoQQYAaEEHgGhBB8BoQQdAQABoQQhAaEEIAGhBCMBAAGhBCUCoQQkAqEEKAEABKEEKgGhBCsBoQQwAQABoQQxAaEEMgGhBDQBAAGhBDYBoQQ1AQAEoQQ5AQABoQQ4AQAEoQM6AQAEoQRFAaEESgEAAaEESwEABKEETQGhBEABoQRSAgABoQRTAgAEoQRVAgABoQReAaEEWAEABKEEYAEAAaEEZgKhBGECAAShBGsBAAGhAaUBAgAEoQRwAgABoQRzAQAEoQR5AQABoQSAAQEABKEEggEBAAGhBIcBAwABoQGzAQEAAaEEjQECpwHMAQAIAASRAQR9AX0CfQN9BGcDACEBA21hcAN0d28BoQMAAQAEoQMBAQABoQMGAQAEIQEDbWFwA29uZQOhBAEBAAShAw8CoQMQAaEDFgEAAaEDFwEAAaEDGAGhAxwBAAGhAx0BoQIaBAAEoQMjAgABoQMpAQABoQMfAQABoQMrAQABoQMvAaEDLQEAAaEDMQIABKEDNQGhAzIBoQM6AaEDPAGhBCMBAAGhAU8BAAGhA0ADoQJCAQABoQNEAgABoQNFAgAEoQJEAQABoQNLAaEEQAEABKEESgEAAaEDWAGhA1MBAAGhA1oBAAGhA10DoQNbAQABoQNhAwABoQNiAQAEoQNmAaEDaAWhA20BAAShA3IBAAGhA3MBAAShA3gBoQN6A6EDfwEAAaEDgwEBAAShA4UBAgABoQOLAQGhA4IBAaEDjQEBoQOOAQEAAaEDjwEBAAShA5ABAaEDkgEBoQOXAQEABKEDmQEBAAGhA5gBAQABoQOgAQEAAaEDngEBaQIAIQEDbWFwA3R3bwGhAwABoQEAAQABoQIBAQAEoQIEAaEDAQKhAQwDAAShAg4BAAShAhMBoQQJBAABoQQVAQABoQIeAaECHAGhBBgBAAShAiICAAShBB4BAAShBB8BAAGhAzwBAAGhBCMCoQM9AqEDPgEAAaECOQEABKECPAGhAkEBAAGhAjoBAAGhAkIBAAShAkQBAAShAksBAAGhA0UCAAShAlMCoQJQAQAEoQJZAaECWgEAAaECYAKhAl8BAAShAmMCAAGhAmoBoQJkAgAEoQRSAQABoQJzAQAEoQRTAQAEoQJ6AQAEoQJ1AQABoQKEAQEABKEChgEBoQJ/AgABoQKLAQEABKECjwECAAGhApUBAQABoQKXAQGhAo0BAQABoQKaAQGhApkBAQABoQKcAQEAAaECnwEBAAShAqEBAaECnQEBAAShAqYBAaECpwEBAAGhAqwBAQABoQKtAQEABKECrwECAAF5AQAhAQNtYXADb25lASEBA21hcAN0d28CAAGhAQABoQMAAaEBBAEAAaEBBQGhAQYBoQMPAaEEAQGhAQoBoQELAaEBDAEABKEBDgEAAaEBDQEABKEBFQEABKEDHAKhBBUBAAShASECAAShAScBoQQWAwAEoQIhAgABoQEvAaECIgEABKEBOAEAAaEBNwEAAaEBPwGhAzoBAAShAUIBoQQjAQABoQFIAQABoQFKAaEDPgEAAaECOgEAAaEBTwIAAaEBUgEAAaECQQGhAVQCoQFWAgABoQFYAQAEoQFcAqEBWgEAAaEBYgShAWMBAAGhAWgBAAGhAWkCAAGhAW4BAAGhAWsBAAShAXABAAShAXcCAAShAX0BAAShAYIBAaEBcgEAAaEBhwEBoQGIAQEABKEBigEBAAShAZABAQAEoQGLAQIABKEBlQEBAAShBGkEAAGhAagBAQAEoQRzAQABoQGvAQKhBHsBAAGhAbMBAgAEoQSAAQEAAaEBuwECAAGhAbYBAqEEiwEBAAShAcIBAQAEoQHHAQEABKEEkAEBpwHRAQEoAAHSAQdkZWVwa2V5AXcJZGVlcHZhbHVloQHMAQFiAAAhAQNtYXADb25lAwABoQACASEBA21hcAN0d28CAAShAAQBoQAGAaEACwEAAaEADQIABKEADAEABKEAEAEABKEAGgEABKEAHwEABKEAFQGhACQBAAGhACoBoQApAaEALAGhAC0BoQAuAaEALwEAAaEAMAIAAaEANAEABKEAMQEABKEANgEAAaEAQAIAAaEAOwGhAEMCAAShAEcBAAShAEwBoQBFAQAEoQBRAQAEoQBXAqEAUgEABKEAXgIAAaEAZAKhAF0BoQBnAqEAaAEABKEAawGhAGoCoQBwAQABoQBzAQAEoQB1AQABoQB6AaEAcgGhAHwBAAShAH4BoQB9AgABoQCFAQEABKEAhwEBAAShAIwBAaEAgwEBAAShAJIBAQAEoQCXAQIABKEAkQEBAAGhAJ0BAQAEoQCiAQEABKEApAECAAGhAK8BAqEAqQEBAAGhALMBAQABBQABALcBAQIA0gHUAQEEAQCRAQMBAKUBAgEAuQE=' // eslint-disable-next-line const oldVal = /** @type {any} */ ({"one":[1,2,3,4],"two":{"deepkey":"deepvalue"}}) const doc = new Y.Doc() Y.applyUpdate(doc, buffer.fromBase64(oldDoc)) t.compare(doc.getMap('map').toJSON(), oldVal) } /** * @param {t.TestCase} tc */ export const testTextDecodingCompatibilityV1 = tc => { const oldDoc = 'BS8EAAUBBHRleHRveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9RAQAATHBBAEEAAHBBAIEAAHEBAMEAAQxdXUKxQQCBANveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQMJBAFveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQMJBAlveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xgMBAwIGaXRhbGljBHRydWXGBAsDAgVjb2xvcgYiIzg4OCLEBAwDAgExxAQNAwIBMsEEDgMCAsYEEAMCBml0YWxpYwRudWxsxgQRAwIFY29sb3IEbnVsbMQDAQQLATHEBBMECwIyOcQEFQQLCzl6anpueXdvaHB4xAQgBAsIY25icmNhcQrBAxADEQHGAR8BIARib2xkBHRydWXGAgACAQRib2xkBG51bGzFAwkECm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GARABEQZpdGFsaWMEdHJ1ZcYELQERBWNvbG9yBiIjODg4IsYBEgETBml0YWxpYwRudWxsxgQvARMFY29sb3IEbnVsbMYCKwIsBGJvbGQEdHJ1ZcYCLQIuBGJvbGQEbnVsbMYCjAECjQEGaXRhbGljBHRydWXGAo4BAo8BBml0YWxpYwRudWxswQA2ADcBxgQ1ADcFY29sb3IGIiM4ODgixgNlA2YFY29sb3IEbnVsbMYDUwNUBGJvbGQEdHJ1ZcQEOANUFjEzMTZ6bHBrbWN0b3FvbWdmdGhicGfGBE4DVARib2xkBG51bGzGAk0CTgZpdGFsaWMEdHJ1ZcYEUAJOBWNvbG9yBiIjODg4IsYCTwJQBml0YWxpYwRudWxsxgRSAlAFY29sb3IEbnVsbMYChAEChQEGaXRhbGljBHRydWXGBFQChQEFY29sb3IGIiM4ODgixgKGAQKHAQZpdGFsaWMEbnVsbMYEVgKHAQVjb2xvcgRudWxsxAMpAyoRMTMyMWFwZ2l2eWRxc2pmc2XFBBIDAm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn0zAwAEAQR0ZXh0AjEyhAMBAzkwboQDBAF4gQMFAoQDBwJyCsQDBAMFBjEyOTd6bcQDDwMFAXbEAxADBQFwwQMRAwUBxAMSAwUFa3pxY2rEAxcDBQJzYcQDGQMFBHNqeQrBAxIDEwHBAAwAEAHEAA0ADgkxMzAyeGNpd2HEAygADgF5xAMpAA4KaGhlenVraXF0dMQDMwAOBWhudGsKxgMoAykEYm9sZAR0cnVlxAM5AykGMTMwNXJswQM/AykCxANBAykDZXlrxgNEAykEYm9sZARudWxsxAMzAzQJMTMwN3R2amllwQNOAzQCxANQAzQDamxoxANTAzQCZ3bEA1UDNAJsYsQDVwM0AmYKxgNBA0IEYm9sZARudWxswQNaA0ICxANcA0ICMDjBA14DQgLEA2ADQgEKxgNhA0IEYm9sZAR0cnVlxQIaAhtveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9wQA3ADgCwQNlADgBxANmADgKMTVteml3YWJ6a8EDcAA4AsQDcgA4BnJybXNjdsEDeAA4AcQCYgJjATHEA3oCYwIzMsQDfAJjCTRyb3J5d3RoccQDhQECYwEKxAOFAQOGARkxMzI1aW9kYnppenhobWxpYnZweXJ4bXEKwQN6A3sBxgOgAQN7BWNvbG9yBiIjODg4IsYDfAN9Bml0YWxpYwRudWxsxgOiAQN9BWNvbG9yBG51bGxSAgAEAQR0ZXh0ATGEAgACMjiEAgIBOYECAwKEAgUBdYQCBgJ0Y4QCCAJqZYECCgKEAgwBaoECDQGBAg4BhAIPAnVmhAIRAQrEAg4CDwgxMjkycXJtZsQCGgIPAmsKxgIGAgcGaXRhbGljBHRydWXGAggCCQZpdGFsaWMEbnVsbMYCEQISBml0YWxpYwR0cnVlxAIfAhIBMcECIAISAsQCIgISAzRoc8QCJQISAXrGAiYCEgZpdGFsaWMEbnVsbMEAFQAWAsQCKQAWATDEAioAFgEwxAIrABYCaHjEAi0AFglvamVldHJqaHjBAjYAFgLEAjgAFgJrcsQCOgAWAXHBAjsAFgHBAjwAFgHEAj0AFgFuxAI+ABYCZQrGAiUCJgZpdGFsaWMEbnVsbMQCQQImAjEzwQJDAiYCxAJFAiYIZGNjeGR5eGfEAk0CJgJ6Y8QCTwImA2Fwb8QCUgImAnRuxAJUAiYBcsQCVQImAmduwQJXAiYCxAJZAiYBCsYCWgImBml0YWxpYwR0cnVlxAI6AjsEMTMwM8QCXwI7A3VodsQCYgI7BmdhbmxuCsUCVQJWb3siaW1hZ2UiOiJodHRwczovL3VzZXItaW1hZ2VzLmdpdGh1YnVzZXJjb250ZW50LmNvbS81NTUzNzU3LzQ4OTc1MzA3LTYxZWZiMTAwLWYwNmQtMTFlOC05MTc3LWVlODk1ZTU5MTZlNS5wbmcifcECPAI9AcECPgI/AcYDFwMYBml0YWxpYwR0cnVlxgJsAxgFY29sb3IGIiM4ODgixgMZAxoGaXRhbGljBG51bGzGAm4DGgVjb2xvcgRudWxswQMQBCkBxAJwBCkKMTMwOXpsZ3ZqeMQCegQpAWfBAnsEKQLGBA0EDgZpdGFsaWMEbnVsbMYCfgQOBWNvbG9yBG51bGzEAn8EDgUxMzEwZ8QChAEEDgJ3c8QChgEEDgZoeHd5Y2jEAowBBA4Ca3HEAo4BBA4Ec2RydcQCkgEEDgRqcWljwQKWAQQOBMQCmgEEDgEKxgKbAQQOBml0YWxpYwR0cnVlxgKcAQQOBWNvbG9yBiIjODg4IsECaAI7AcQCCgEBFjEzMThqd3NramFiZG5kcmRsbWphZQrGA1UDVgRib2xkBHRydWXGA1cDWARib2xkBG51bGzGAEAAQQZpdGFsaWMEdHJ1ZcYCtwEAQQRib2xkBG51bGzEArgBAEESMTMyNnJwY3pucWFob3BjcnRkxgLKAQBBBml0YWxpYwRudWxsxgLLAQBBBGJvbGQEdHJ1ZRkBAMUCAgIDb3siaW1hZ2UiOiJodHRwczovL3VzZXItaW1hZ2VzLmdpdGh1YnVzZXJjb250ZW50LmNvbS81NTUzNzU3LzQ4OTc1MzA3LTYxZWZiMTAwLWYwNmQtMTFlOC05MTc3LWVlODk1ZTU5MTZlNS5wbmcifcQCCgILBzEyOTN0agrGABgAGQRib2xkBHRydWXGAA0ADgRib2xkBG51bGxEAgAHMTMwNnJ1cMQBEAIAAnVqxAESAgANaWtrY2pucmNwc2Nrd8QBHwIAAQrFBBMEFG97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3FAx0DBW97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GAlICUwRib2xkBHRydWXGAlQCVQRib2xkBG51bGzGAnsCfAZpdGFsaWMEdHJ1ZcYBJQJ8BWNvbG9yBiIjODg4IsYBJgJ8BGJvbGQEbnVsbMQBJwJ8CjEzMTRweWNhdnXGATECfAZpdGFsaWMEbnVsbMYBMgJ8BWNvbG9yBG51bGzBATMCfAHFADEAMm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GADUANgZpdGFsaWMEdHJ1ZcEANwA4AcQAMgAzEzEzMjJybmJhb2tvcml4ZW52cArEAgUCBhcxMzIzbnVjdnhzcWx6bndsZmF2bXBjCsYDDwMQBGJvbGQEdHJ1ZR0AAMQEAwQEDTEyOTVxZnJ2bHlmYXDEAAwEBAFjxAANBAQCanbBAAwADQHEABAADQEywQARAA0ExAAVAA0DZHZmxAAYAA0BYcYCAwIEBml0YWxpYwR0cnVlwQAaAgQCxAAcAgQEMDRrdcYAIAIEBml0YWxpYwRudWxsxQQgBCFveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQJAABZveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xAQVBBYGMTMxMWtrxAIqAisIMTMxMnFyd3TEADECKwFixAAyAisDcnhxxAA1AisBasQANgIrAXjEADcCKwZkb3ZhbwrEAgAEKwMxMzHEAEAEKwkzYXhoa3RoaHXGAnoCewRib2xkBG51bGzFAEoCe297ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GAEsCewRib2xkBHRydWXEAl8CYBExMzE3cGZjeWhrc3JrcGt0CsQBHwQqCzEzMTliY2Nna3AKxAKSAQKTARUxMzIwY29oYnZjcmtycGpuZ2RvYwoFBAQCAg8CKQE1AQADEAESBBsCAwsGAhIBHgJAAk8CWwJfAmQDcQJ5AaABAQIOBAILAg4CIQIoAjcCPAJEAlgCagJwAXwClwEEngEBAQI0ATcB' // eslint-disable-next-line const oldVal = [{"insert":"1306rup"},{"insert":"uj","attributes":{"italic":true,"color":"#888"}},{"insert":"ikkcjnrcpsckw1319bccgkp\n"},{"insert":"\n1131","attributes":{"bold":true}},{"insert":"1326rpcznqahopcrtd","attributes":{"italic":true}},{"insert":"3axhkthhu","attributes":{"bold":true}},{"insert":"28"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"9"},{"insert":"04ku","attributes":{"italic":true}},{"insert":"1323nucvxsqlznwlfavmpc\nu"},{"insert":"tc","attributes":{"italic":true}},{"insert":"je1318jwskjabdndrdlmjae\n1293tj\nj1292qrmf"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"k\nuf"},{"insert":"14hs","attributes":{"italic":true}},{"insert":"13dccxdyxg"},{"insert":"zc","attributes":{"italic":true,"color":"#888"}},{"insert":"apo"},{"insert":"tn","attributes":{"bold":true}},{"insert":"r"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"gn\n"},{"insert":"z","attributes":{"italic":true}},{"insert":"\n121"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"291311kk9zjznywohpx"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"cnbrcaq\n"},{"insert":"1","attributes":{"italic":true,"color":"#888"}},{"insert":"1310g"},{"insert":"ws","attributes":{"italic":true,"color":"#888"}},{"insert":"hxwych"},{"insert":"kq","attributes":{"italic":true}},{"insert":"sdru1320cohbvcrkrpjngdoc\njqic\n"},{"insert":"2","attributes":{"italic":true,"color":"#888"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"90n1297zm"},{"insert":"v1309zlgvjx","attributes":{"bold":true}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"g","attributes":{"bold":true}},{"insert":"1314pycavu","attributes":{"italic":true,"color":"#888"}},{"insert":"pkzqcj"},{"insert":"sa","attributes":{"italic":true,"color":"#888"}},{"insert":"sjy\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"xr\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"1"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"1295qfrvlyfap201312qrwt"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"b1322rnbaokorixenvp\nrxq"},{"insert":"j","attributes":{"italic":true}},{"insert":"x","attributes":{"italic":true,"color":"#888"}},{"insert":"15mziwabzkrrmscvdovao\n0","attributes":{"italic":true}},{"insert":"hx","attributes":{"italic":true,"bold":true}},{"insert":"ojeetrjhxkr13031317pfcyhksrkpkt\nuhv1","attributes":{"italic":true}},{"insert":"32","attributes":{"italic":true,"color":"#888"}},{"insert":"4rorywthq1325iodbzizxhmlibvpyrxmq\n\nganln\nqne\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"dvf"},{"insert":"ac","attributes":{"bold":true}},{"insert":"1302xciwa"},{"insert":"1305rl","attributes":{"bold":true}},{"insert":"08\n"},{"insert":"eyk","attributes":{"bold":true}},{"insert":"y1321apgivydqsjfsehhezukiqtt1307tvjiejlh"},{"insert":"1316zlpkmctoqomgfthbpg","attributes":{"bold":true}},{"insert":"gv"},{"insert":"lb","attributes":{"bold":true}},{"insert":"f\nhntk\njv1uu\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}}] const doc = new Y.Doc() Y.applyUpdate(doc, buffer.fromBase64(oldDoc)) t.compare(doc.getText('text').toDelta(), oldVal) } yjs-13.6.8/tests/doc.tests.js000066400000000000000000000214051450200430400160160ustar00rootroot00000000000000 import * as Y from '../src/index.js' import * as t from 'lib0/testing' /** * @param {t.TestCase} _tc */ export const testAfterTransactionRecursion = _tc => { const ydoc = new Y.Doc() const yxml = ydoc.getXmlFragment('') ydoc.on('afterTransaction', tr => { if (tr.origin === 'test') { yxml.toJSON() } }) ydoc.transact(_tr => { for (let i = 0; i < 15000; i++) { yxml.push([new Y.XmlText('a')]) } }, 'test') } /** * @param {t.TestCase} _tc */ export const testOriginInTransaction = _tc => { const doc = new Y.Doc() const ytext = doc.getText() /** * @type {Array} */ const origins = [] doc.on('afterTransaction', (tr) => { origins.push(tr.origin) if (origins.length <= 1) { ytext.toDelta(Y.snapshot(doc)) // adding a snapshot forces toDelta to create a cleanup transaction doc.transact(() => { ytext.insert(0, 'a') }, 'nested') } }) doc.transact(() => { ytext.insert(0, '0') }, 'first') t.compareArrays(origins, ['first', 'cleanup', 'nested']) } /** * Client id should be changed when an instance receives updates from another client using the same client id. * * @param {t.TestCase} _tc */ export const testClientIdDuplicateChange = _tc => { const doc1 = new Y.Doc() doc1.clientID = 0 const doc2 = new Y.Doc() doc2.clientID = 0 t.assert(doc2.clientID === doc1.clientID) doc1.getArray('a').insert(0, [1, 2]) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1)) t.assert(doc2.clientID !== doc1.clientID) } /** * @param {t.TestCase} _tc */ export const testGetTypeEmptyId = _tc => { const doc1 = new Y.Doc() doc1.getText('').insert(0, 'h') doc1.getText().insert(1, 'i') const doc2 = new Y.Doc() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1)) t.assert(doc2.getText().toString() === 'hi') t.assert(doc2.getText('').toString() === 'hi') } /** * @param {t.TestCase} _tc */ export const testToJSON = _tc => { const doc = new Y.Doc() t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object') const arr = doc.getArray('array') arr.push(['test1']) const map = doc.getMap('map') map.set('k1', 'v1') const map2 = new Y.Map() map.set('k2', map2) map2.set('m2k1', 'm2v1') t.compare(doc.toJSON(), { array: ['test1'], map: { k1: 'v1', k2: { m2k1: 'm2v1' } } }, 'doc.toJSON has array and recursive map') } /** * @param {t.TestCase} _tc */ export const testSubdoc = _tc => { const doc = new Y.Doc() doc.load() // doesn't do anything { /** * @type {Array|null} */ let event = /** @type {any} */ (null) doc.on('subdocs', subdocs => { event = [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)] }) const subdocs = doc.getMap('mysubdocs') const docA = new Y.Doc({ guid: 'a' }) docA.load() subdocs.set('a', docA) t.compare(event, [['a'], [], ['a']]) event = null subdocs.get('a').load() t.assert(event === null) event = null subdocs.get('a').destroy() t.compare(event, [['a'], ['a'], []]) subdocs.get('a').load() t.compare(event, [[], [], ['a']]) subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false })) t.compare(event, [['a'], [], []]) subdocs.get('b').load() t.compare(event, [[], [], ['a']]) const docC = new Y.Doc({ guid: 'c' }) docC.load() subdocs.set('c', docC) t.compare(event, [['c'], [], ['c']]) t.compare(Array.from(doc.getSubdocGuids()), ['a', 'c']) } const doc2 = new Y.Doc() { t.compare(Array.from(doc2.getSubdocs()), []) /** * @type {Array|null} */ let event = /** @type {any} */ (null) doc2.on('subdocs', subdocs => { event = [Array.from(subdocs.added).map(d => d.guid), Array.from(subdocs.removed).map(d => d.guid), Array.from(subdocs.loaded).map(d => d.guid)] }) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) t.compare(event, [['a', 'a', 'c'], [], []]) doc2.getMap('mysubdocs').get('a').load() t.compare(event, [[], [], ['a']]) t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c']) doc2.getMap('mysubdocs').delete('a') t.compare(event, [[], ['a'], []]) t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c']) } } /** * @param {t.TestCase} _tc */ export const testSubdocLoadEdgeCases = _tc => { const ydoc = new Y.Doc() const yarray = ydoc.getArray() const subdoc1 = new Y.Doc() /** * @type {any} */ let lastEvent = null ydoc.on('subdocs', event => { lastEvent = event }) yarray.insert(0, [subdoc1]) t.assert(subdoc1.shouldLoad) t.assert(subdoc1.autoLoad === false) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1)) t.assert(lastEvent !== null && lastEvent.added.has(subdoc1)) // destroy and check whether lastEvent adds it again to added (it shouldn't) subdoc1.destroy() const subdoc2 = yarray.get(0) t.assert(subdoc1 !== subdoc2) t.assert(lastEvent !== null && lastEvent.added.has(subdoc2)) t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2)) // load subdoc2.load() t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2)) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2)) // apply from remote const ydoc2 = new Y.Doc() ydoc2.on('subdocs', event => { lastEvent = event }) Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc)) const subdoc3 = ydoc2.getArray().get(0) t.assert(subdoc3.shouldLoad === false) t.assert(subdoc3.autoLoad === false) t.assert(lastEvent !== null && lastEvent.added.has(subdoc3)) t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc3)) // load subdoc3.load() t.assert(subdoc3.shouldLoad) t.assert(lastEvent !== null && !lastEvent.added.has(subdoc3)) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3)) } /** * @param {t.TestCase} _tc */ export const testSubdocLoadEdgeCasesAutoload = _tc => { const ydoc = new Y.Doc() const yarray = ydoc.getArray() const subdoc1 = new Y.Doc({ autoLoad: true }) /** * @type {any} */ let lastEvent = null ydoc.on('subdocs', event => { lastEvent = event }) yarray.insert(0, [subdoc1]) t.assert(subdoc1.shouldLoad) t.assert(subdoc1.autoLoad) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1)) t.assert(lastEvent !== null && lastEvent.added.has(subdoc1)) // destroy and check whether lastEvent adds it again to added (it shouldn't) subdoc1.destroy() const subdoc2 = yarray.get(0) t.assert(subdoc1 !== subdoc2) t.assert(lastEvent !== null && lastEvent.added.has(subdoc2)) t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2)) // load subdoc2.load() t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2)) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2)) // apply from remote const ydoc2 = new Y.Doc() ydoc2.on('subdocs', event => { lastEvent = event }) Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc)) const subdoc3 = ydoc2.getArray().get(0) t.assert(subdoc1.shouldLoad) t.assert(subdoc1.autoLoad) t.assert(lastEvent !== null && lastEvent.added.has(subdoc3)) t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3)) } /** * @param {t.TestCase} _tc */ export const testSubdocsUndo = _tc => { const ydoc = new Y.Doc() const elems = ydoc.getXmlFragment() const undoManager = new Y.UndoManager(elems) const subdoc = new Y.Doc() // @ts-ignore elems.insert(0, [subdoc]) undoManager.undo() undoManager.redo() t.assert(elems.length === 1) } /** * @param {t.TestCase} _tc */ export const testLoadDocsEvent = async _tc => { const ydoc = new Y.Doc() t.assert(ydoc.isLoaded === false) let loadedEvent = false ydoc.on('load', () => { loadedEvent = true }) ydoc.emit('load', [ydoc]) await ydoc.whenLoaded t.assert(loadedEvent) t.assert(ydoc.isLoaded) } /** * @param {t.TestCase} _tc */ export const testSyncDocsEvent = async _tc => { const ydoc = new Y.Doc() t.assert(ydoc.isLoaded === false) t.assert(ydoc.isSynced === false) let loadedEvent = false ydoc.once('load', () => { loadedEvent = true }) let syncedEvent = false ydoc.once('sync', /** @param {any} isSynced */ (isSynced) => { syncedEvent = true t.assert(isSynced) }) ydoc.emit('sync', [true, ydoc]) await ydoc.whenLoaded const oldWhenSynced = ydoc.whenSynced await ydoc.whenSynced t.assert(loadedEvent) t.assert(syncedEvent) t.assert(ydoc.isLoaded) t.assert(ydoc.isSynced) let loadedEvent2 = false ydoc.on('load', () => { loadedEvent2 = true }) let syncedEvent2 = false ydoc.on('sync', (isSynced) => { syncedEvent2 = true t.assert(isSynced === false) }) ydoc.emit('sync', [false, ydoc]) t.assert(!loadedEvent2) t.assert(syncedEvent2) t.assert(ydoc.isLoaded) t.assert(!ydoc.isSynced) t.assert(ydoc.whenSynced !== oldWhenSynced) } yjs-13.6.8/tests/encoding.tests.js000066400000000000000000000061201450200430400170340ustar00rootroot00000000000000import * as t from 'lib0/testing' import * as promise from 'lib0/promise' import { contentRefs, readContentBinary, readContentDeleted, readContentString, readContentJSON, readContentEmbed, readContentType, readContentFormat, readContentAny, readContentDoc, Doc, PermanentUserData, encodeStateAsUpdate, applyUpdate } from '../src/internals.js' import * as Y from '../src/index.js' /** * @param {t.TestCase} tc */ export const testStructReferences = tc => { t.assert(contentRefs.length === 11) t.assert(contentRefs[1] === readContentDeleted) t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json? t.assert(contentRefs[3] === readContentBinary) t.assert(contentRefs[4] === readContentString) t.assert(contentRefs[5] === readContentEmbed) t.assert(contentRefs[6] === readContentFormat) t.assert(contentRefs[7] === readContentType) t.assert(contentRefs[8] === readContentAny) t.assert(contentRefs[9] === readContentDoc) // contentRefs[10] is reserved for Skip structs } /** * There is some custom encoding/decoding happening in PermanentUserData. * This is why it landed here. * * @param {t.TestCase} tc */ export const testPermanentUserData = async tc => { const ydoc1 = new Doc() const ydoc2 = new Doc() const pd1 = new PermanentUserData(ydoc1) const pd2 = new PermanentUserData(ydoc2) pd1.setUserMapping(ydoc1, ydoc1.clientID, 'user a') pd2.setUserMapping(ydoc2, ydoc2.clientID, 'user b') ydoc1.getText().insert(0, 'xhi') ydoc1.getText().delete(0, 1) ydoc2.getText().insert(0, 'hxxi') ydoc2.getText().delete(1, 2) await promise.wait(10) applyUpdate(ydoc2, encodeStateAsUpdate(ydoc1)) applyUpdate(ydoc1, encodeStateAsUpdate(ydoc2)) // now sync a third doc with same name as doc1 and then create PermanentUserData const ydoc3 = new Doc() applyUpdate(ydoc3, encodeStateAsUpdate(ydoc1)) const pd3 = new PermanentUserData(ydoc3) pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a') } /** * Reported here: https://github.com/yjs/yjs/issues/308 * @param {t.TestCase} tc */ export const testDiffStateVectorOfUpdateIsEmpty = tc => { const ydoc = new Y.Doc() /** * @type {any} */ let sv = null ydoc.getText().insert(0, 'a') ydoc.on('update', update => { sv = Y.encodeStateVectorFromUpdate(update) }) // should produce an update with an empty state vector (because previous ops are missing) ydoc.getText().insert(0, 'a') t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0) } /** * Reported here: https://github.com/yjs/yjs/issues/308 * @param {t.TestCase} tc */ export const testDiffStateVectorOfUpdateIgnoresSkips = tc => { const ydoc = new Y.Doc() /** * @type {Array} */ const updates = [] ydoc.on('update', update => { updates.push(update) }) ydoc.getText().insert(0, 'a') ydoc.getText().insert(0, 'b') ydoc.getText().insert(0, 'c') const update13 = Y.mergeUpdates([updates[0], updates[2]]) const sv = Y.encodeStateVectorFromUpdate(update13) const state = Y.decodeStateVector(sv) t.assert(state.get(ydoc.clientID) === 1) t.assert(state.size === 1) } yjs-13.6.8/tests/index.js000066400000000000000000000016641450200430400152240ustar00rootroot00000000000000/* eslint-env node */ import * as map from './y-map.tests.js' import * as array from './y-array.tests.js' import * as text from './y-text.tests.js' import * as xml from './y-xml.tests.js' import * as encoding from './encoding.tests.js' import * as undoredo from './undo-redo.tests.js' import * as compatibility from './compatibility.tests.js' import * as doc from './doc.tests.js' import * as snapshot from './snapshot.tests.js' import * as updates from './updates.tests.js' import * as relativePositions from './relativePositions.tests.js' import { runTests } from 'lib0/testing' import { isBrowser, isNode } from 'lib0/environment' import * as log from 'lib0/logging' if (isBrowser) { log.createVConsole(document.body) } runTests({ doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions }).then(success => { /* istanbul ignore next */ if (isNode) { process.exit(success ? 0 : 1) } }) yjs-13.6.8/tests/relativePositions.tests.js000066400000000000000000000052701450200430400207760ustar00rootroot00000000000000 import * as Y from '../src/index.js' import * as t from 'lib0/testing' /** * @param {Y.Text} ytext */ const checkRelativePositions = ytext => { // test if all positions are encoded and restored correctly for (let i = 0; i < ytext.length; i++) { // for all types of associations.. for (let assoc = -1; assoc < 2; assoc++) { const rpos = Y.createRelativePositionFromTypeIndex(ytext, i, assoc) const encodedRpos = Y.encodeRelativePosition(rpos) const decodedRpos = Y.decodeRelativePosition(encodedRpos) const absPos = /** @type {Y.AbsolutePosition} */ (Y.createAbsolutePositionFromRelativePosition(decodedRpos, /** @type {Y.Doc} */ (ytext.doc))) t.assert(absPos.index === i) t.assert(absPos.assoc === assoc) } } } /** * @param {t.TestCase} tc */ export const testRelativePositionCase1 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, '1') ytext.insert(0, 'abc') ytext.insert(0, 'z') ytext.insert(0, 'y') ytext.insert(0, 'x') checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionCase2 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, 'abc') checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionCase3 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, 'abc') ytext.insert(0, '1') ytext.insert(0, 'xyz') checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionCase4 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, '1') checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionCase5 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, '2') ytext.insert(0, '1') checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionCase6 = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() checkRelativePositions(ytext) } /** * @param {t.TestCase} tc */ export const testRelativePositionAssociationDifference = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText() ytext.insert(0, '2') ytext.insert(0, '1') const rposRight = Y.createRelativePositionFromTypeIndex(ytext, 1, 0) const rposLeft = Y.createRelativePositionFromTypeIndex(ytext, 1, -1) ytext.insert(1, 'x') const posRight = Y.createAbsolutePositionFromRelativePosition(rposRight, ydoc) const posLeft = Y.createAbsolutePositionFromRelativePosition(rposLeft, ydoc) t.assert(posRight != null && posRight.index === 2) t.assert(posLeft != null && posLeft.index === 1) } yjs-13.6.8/tests/snapshot.tests.js000066400000000000000000000130251450200430400171070ustar00rootroot00000000000000import * as Y from '../src/index.js' import * as t from 'lib0/testing' import { init } from './testHelper.js' /** * @param {t.TestCase} _tc */ export const testBasic = _tc => { const ydoc = new Y.Doc({ gc: false }) ydoc.getText().insert(0, 'world!') const snapshot = Y.snapshot(ydoc) ydoc.getText().insert(0, 'hello ') const restored = Y.createDocFromSnapshot(ydoc, snapshot) t.assert(restored.getText().toString() === 'world!') } /** * @param {t.TestCase} _tc */ export const testBasicRestoreSnapshot = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['hello']) const snap = Y.snapshot(doc) doc.getArray('array').insert(1, ['world']) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toArray(), ['hello']) t.compare(doc.getArray('array').toArray(), ['hello', 'world']) } /** * @param {t.TestCase} _tc */ export const testEmptyRestoreSnapshot = _tc => { const doc = new Y.Doc({ gc: false }) const snap = Y.snapshot(doc) snap.sv.set(9999, 0) doc.getArray().insert(0, ['world']) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray().toArray(), []) t.compare(doc.getArray().toArray(), ['world']) // now this snapshot reflects the latest state. It shoult still work. const snap2 = Y.snapshot(doc) const docRestored2 = Y.createDocFromSnapshot(doc, snap2) t.compare(docRestored2.getArray().toArray(), ['world']) } /** * @param {t.TestCase} _tc */ export const testRestoreSnapshotWithSubType = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, [new Y.Map()]) const subMap = doc.getArray('array').get(0) subMap.set('key1', 'value1') const snap = Y.snapshot(doc) subMap.set('key2', 'value2') const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toJSON(), [{ key1: 'value1' }]) t.compare(doc.getArray('array').toJSON(), [{ key1: 'value1', key2: 'value2' }]) } /** * @param {t.TestCase} _tc */ export const testRestoreDeletedItem1 = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['item1', 'item2']) const snap = Y.snapshot(doc) doc.getArray('array').delete(0) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2']) t.compare(doc.getArray('array').toArray(), ['item2']) } /** * @param {t.TestCase} _tc */ export const testRestoreLeftItem = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['item1']) doc.getMap('map').set('test', 1) doc.getArray('array').insert(0, ['item0']) const snap = Y.snapshot(doc) doc.getArray('array').delete(1) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1']) t.compare(doc.getArray('array').toArray(), ['item0']) } /** * @param {t.TestCase} _tc */ export const testDeletedItemsBase = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['item1']) doc.getArray('array').delete(0) const snap = Y.snapshot(doc) doc.getArray('array').insert(0, ['item0']) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toArray(), []) t.compare(doc.getArray('array').toArray(), ['item0']) } /** * @param {t.TestCase} _tc */ export const testDeletedItems2 = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['item1', 'item2', 'item3']) doc.getArray('array').delete(1) const snap = Y.snapshot(doc) doc.getArray('array').insert(0, ['item0']) const docRestored = Y.createDocFromSnapshot(doc, snap) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3']) t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3']) } /** * @param {t.TestCase} tc */ export const testDependentChanges = tc => { const { array0, array1, testConnector } = init(tc, { users: 2 }) if (!array0.doc) { throw new Error('no document 0') } if (!array1.doc) { throw new Error('no document 1') } /** * @type {Y.Doc} */ const doc0 = array0.doc /** * @type {Y.Doc} */ const doc1 = array1.doc doc0.gc = false doc1.gc = false array0.insert(0, ['user1item1']) testConnector.syncAll() array1.insert(1, ['user2item1']) testConnector.syncAll() const snap = Y.snapshot(array0.doc) array0.insert(2, ['user1item2']) testConnector.syncAll() array1.insert(3, ['user2item2']) testConnector.syncAll() const docRestored0 = Y.createDocFromSnapshot(array0.doc, snap) t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1']) const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap) t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1']) } /** * @param {t.TestCase} _tc */ export const testContainsUpdate = _tc => { const ydoc = new Y.Doc() /** * @type {Array} */ const updates = [] ydoc.on('update', update => { updates.push(update) }) const yarr = ydoc.getArray() const snapshot1 = Y.snapshot(ydoc) yarr.insert(0, [1]) const snapshot2 = Y.snapshot(ydoc) yarr.delete(0, 1) const snapshotFinal = Y.snapshot(ydoc) t.assert(!Y.snapshotContainsUpdate(snapshot1, updates[0])) t.assert(!Y.snapshotContainsUpdate(snapshot2, updates[1])) t.assert(Y.snapshotContainsUpdate(snapshot2, updates[0])) t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[0])) t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[1])) } yjs-13.6.8/tests/testHelper.js000066400000000000000000000336601450200430400162350ustar00rootroot00000000000000 import * as t from 'lib0/testing' import * as prng from 'lib0/prng' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' import * as syncProtocol from 'y-protocols/sync' import * as object from 'lib0/object' import * as map from 'lib0/map' import * as Y from '../src/index.js' export * from '../src/index.js' if (typeof window !== 'undefined') { // @ts-ignore window.Y = Y // eslint-disable-line } /** * @param {TestYInstance} y // publish message created by `y` to all other online clients * @param {Uint8Array} m */ const broadcastMessage = (y, m) => { if (y.tc.onlineConns.has(y)) { y.tc.onlineConns.forEach(remoteYInstance => { if (remoteYInstance !== y) { remoteYInstance._receive(m, y) } }) } } export let useV2 = false export const encV1 = { encodeStateAsUpdate: Y.encodeStateAsUpdate, mergeUpdates: Y.mergeUpdates, applyUpdate: Y.applyUpdate, logUpdate: Y.logUpdate, updateEventName: 'update', diffUpdate: Y.diffUpdate } export const encV2 = { encodeStateAsUpdate: Y.encodeStateAsUpdateV2, mergeUpdates: Y.mergeUpdatesV2, applyUpdate: Y.applyUpdateV2, logUpdate: Y.logUpdateV2, updateEventName: 'updateV2', diffUpdate: Y.diffUpdateV2 } export let enc = encV1 const useV1Encoding = () => { useV2 = false enc = encV1 } const useV2Encoding = () => { console.error('sync protocol doesnt support v2 protocol yet, fallback to v1 encoding') // @Todo useV2 = false enc = encV1 } export class TestYInstance extends Y.Doc { /** * @param {TestConnector} testConnector * @param {number} clientID */ constructor (testConnector, clientID) { super() this.userID = clientID // overwriting clientID /** * @type {TestConnector} */ this.tc = testConnector /** * @type {Map>} */ this.receiving = new Map() testConnector.allConns.add(this) /** * The list of received updates. * We are going to merge them later using Y.mergeUpdates and check if the resulting document is correct. * @type {Array} */ this.updates = [] // set up observe on local model this.on(enc.updateEventName, /** @param {Uint8Array} update @param {any} origin */ (update, origin) => { if (origin !== testConnector) { const encoder = encoding.createEncoder() syncProtocol.writeUpdate(encoder, update) broadcastMessage(this, encoding.toUint8Array(encoder)) } this.updates.push(update) }) this.connect() } /** * Disconnect from TestConnector. */ disconnect () { this.receiving = new Map() this.tc.onlineConns.delete(this) } /** * Append yourself to the list of known Y instances in testconnector. * Also initiate sync with all clients. */ connect () { if (!this.tc.onlineConns.has(this)) { this.tc.onlineConns.add(this) const encoder = encoding.createEncoder() syncProtocol.writeSyncStep1(encoder, this) // publish SyncStep1 broadcastMessage(this, encoding.toUint8Array(encoder)) this.tc.onlineConns.forEach(remoteYInstance => { if (remoteYInstance !== this) { // remote instance sends instance to this instance const encoder = encoding.createEncoder() syncProtocol.writeSyncStep1(encoder, remoteYInstance) this._receive(encoding.toUint8Array(encoder), remoteYInstance) } }) } } /** * Receive a message from another client. This message is only appended to the list of receiving messages. * TestConnector decides when this client actually reads this message. * * @param {Uint8Array} message * @param {TestYInstance} remoteClient */ _receive (message, remoteClient) { map.setIfUndefined(this.receiving, remoteClient, () => /** @type {Array} */ ([])).push(message) } } /** * Keeps track of TestYInstances. * * The TestYInstances add/remove themselves from the list of connections maiained in this object. * I think it makes sense. Deal with it. */ export class TestConnector { /** * @param {prng.PRNG} gen */ constructor (gen) { /** * @type {Set} */ this.allConns = new Set() /** * @type {Set} */ this.onlineConns = new Set() /** * @type {prng.PRNG} */ this.prng = gen } /** * Create a new Y instance and add it to the list of connections * @param {number} clientID */ createY (clientID) { return new TestYInstance(this, clientID) } /** * Choose random connection and flush a random message from a random sender. * * If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise. * @return {boolean} */ flushRandomMessage () { const gen = this.prng const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0) if (conns.length > 0) { const receiver = prng.oneOf(gen, conns) const [sender, messages] = prng.oneOf(gen, Array.from(receiver.receiving)) const m = messages.shift() if (messages.length === 0) { receiver.receiving.delete(sender) } if (m === undefined) { return this.flushRandomMessage() } const encoder = encoding.createEncoder() // console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver)) // do not publish data created when this function is executed (could be ss2 or update message) syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc) if (encoding.length(encoder) > 0) { // send reply message sender._receive(encoding.toUint8Array(encoder), receiver) } return true } return false } /** * @return {boolean} True iff this function actually flushed something */ flushAllMessages () { let didSomething = false while (this.flushRandomMessage()) { didSomething = true } return didSomething } reconnectAll () { this.allConns.forEach(conn => conn.connect()) } disconnectAll () { this.allConns.forEach(conn => conn.disconnect()) } syncAll () { this.reconnectAll() this.flushAllMessages() } /** * @return {boolean} Whether it was possible to disconnect a randon connection. */ disconnectRandom () { if (this.onlineConns.size === 0) { return false } prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect() return true } /** * @return {boolean} Whether it was possible to reconnect a random connection. */ reconnectRandom () { /** * @type {Array} */ const reconnectable = [] this.allConns.forEach(conn => { if (!this.onlineConns.has(conn)) { reconnectable.push(conn) } }) if (reconnectable.length === 0) { return false } prng.oneOf(this.prng, reconnectable).connect() return true } } /** * @template T * @param {t.TestCase} tc * @param {{users?:number}} conf * @param {InitTestObjectCallback} [initTestObject] * @return {{testObjects:Array,testConnector:TestConnector,users:Array,array0:Y.Array,array1:Y.Array,array2:Y.Array,map0:Y.Map,map1:Y.Map,map2:Y.Map,map3:Y.Map,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}} */ export const init = (tc, { users = 5 } = {}, initTestObject) => { /** * @type {Object} */ const result = { users: [] } const gen = tc.prng // choose an encoding approach at random if (prng.bool(gen)) { useV2Encoding() } else { useV1Encoding() } const testConnector = new TestConnector(gen) result.testConnector = testConnector for (let i = 0; i < users; i++) { const y = testConnector.createY(i) y.clientID = i result.users.push(y) result['array' + i] = y.getArray('array') result['map' + i] = y.getMap('map') result['xml' + i] = y.get('xml', Y.XmlElement) result['text' + i] = y.getText('text') } testConnector.syncAll() result.testObjects = result.users.map(initTestObject || (() => null)) useV1Encoding() return /** @type {any} */ (result) } /** * 1. reconnect and flush all * 2. user 0 gc * 3. get type content * 4. disconnect & reconnect all (so gc is propagated) * 5. compare os, ds, ss * * @param {Array} users */ export const compare = users => { users.forEach(u => u.connect()) while (users[0].tc.flushAllMessages()) {} // eslint-disable-line // For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users" // This ensures that mergeUpdates works correctly const mergedDocs = users.map(user => { const ydoc = new Y.Doc() enc.applyUpdate(ydoc, enc.mergeUpdates(user.updates)) return ydoc }) users.push(.../** @type {any} */(mergedDocs)) const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString()) const userTextValues = users.map(u => u.getText('text').toDelta()) for (const u of users) { t.assert(u.store.pendingDs === null) t.assert(u.store.pendingStructs === null) } // Test Array iterator t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array'))) // Test Map iterator const ymapkeys = Array.from(users[0].getMap('map').keys()) t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length) ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key))) /** * @type {Object} */ const mapRes = {} for (const [k, v] of users[0].getMap('map')) { mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v } t.compare(userMapValues[0], mapRes) // Compare all users for (let i = 0; i < users.length - 1; i++) { t.compare(userArrayValues[i].length, users[i].getArray('array').length) t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length) t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => { if (a instanceof Y.AbstractType) { t.compare(a.toJSON(), b.toJSON()) } else if (a !== b) { t.fail('Deltas dont match') } return true }) t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1])) Y.equalDeleteSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store)) compareStructStores(users[i].store, users[i + 1].store) t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1]))) } users.map(u => u.destroy()) } /** * @param {Y.Item?} a * @param {Y.Item?} b * @return {boolean} */ export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) /** * @param {import('../src/internals.js').StructStore} ss1 * @param {import('../src/internals.js').StructStore} ss2 */ export const compareStructStores = (ss1, ss2) => { t.assert(ss1.clients.size === ss2.clients.size) for (const [client, structs1] of ss1.clients) { const structs2 = /** @type {Array} */ (ss2.clients.get(client)) t.assert(structs2 !== undefined && structs1.length === structs2.length) for (let i = 0; i < structs1.length; i++) { const s1 = structs1[i] const s2 = structs2[i] // checks for abstract struct if ( s1.constructor !== s2.constructor || !Y.compareIDs(s1.id, s2.id) || s1.deleted !== s2.deleted || // @ts-ignore s1.length !== s2.length ) { t.fail('Structs dont match') } if (s1 instanceof Y.Item) { if ( !(s2 instanceof Y.Item) || !((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) || !compareItemIDs(s1.right, s2.right) || !Y.compareIDs(s1.origin, s2.origin) || !Y.compareIDs(s1.rightOrigin, s2.rightOrigin) || s1.parentSub !== s2.parentSub ) { return t.fail('Items dont match') } // make sure that items are connected correctly t.assert(s1.left === null || s1.left.right === s1) t.assert(s1.right === null || s1.right.left === s1) t.assert(s2.left === null || s2.left.right === s2) t.assert(s2.right === null || s2.right.left === s2) } } } } /** * @template T * @callback InitTestObjectCallback * @param {TestYInstance} y * @return {T} */ /** * @template T * @param {t.TestCase} tc * @param {Array} mods * @param {number} iterations * @param {InitTestObjectCallback} [initTestObject] */ export const applyRandomTests = (tc, mods, iterations, initTestObject) => { const gen = tc.prng const result = init(tc, { users: 5 }, initTestObject) const { testConnector, users } = result for (let i = 0; i < iterations; i++) { if (prng.int32(gen, 0, 100) <= 2) { // 2% chance to disconnect/reconnect a random user if (prng.bool(gen)) { testConnector.disconnectRandom() } else { testConnector.reconnectRandom() } } else if (prng.int32(gen, 0, 100) <= 1) { // 1% chance to flush all testConnector.flushAllMessages() } else if (prng.int32(gen, 0, 100) <= 50) { // 50% chance to flush a random message testConnector.flushRandomMessage() } const user = prng.int32(gen, 0, users.length - 1) const test = prng.oneOf(gen, mods) test(users[user], gen, result.testObjects[user]) } compare(users) return result } yjs-13.6.8/tests/undo-redo.tests.js000066400000000000000000000461371450200430400171560ustar00rootroot00000000000000import { init } from './testHelper.js' // eslint-disable-line import * as Y from '../src/index.js' import * as t from 'lib0/testing' /** * @param {t.TestCase} tc */ export const testInfiniteCaptureTimeout = tc => { const { array0 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(array0, { captureTimeout: Number.MAX_VALUE }) array0.push([1, 2, 3]) undoManager.stopCapturing() array0.push([4, 5, 6]) undoManager.undo() t.compare(array0.toArray(), [1, 2, 3]) } /** * @param {t.TestCase} tc */ export const testUndoText = tc => { const { testConnector, text0, text1 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(text0) // items that are added & deleted in the same transaction won't be undo text0.insert(0, 'test') text0.delete(0, 4) undoManager.undo() t.assert(text0.toString() === '') // follow redone items text0.insert(0, 'a') undoManager.stopCapturing() text0.delete(0, 1) undoManager.stopCapturing() undoManager.undo() t.assert(text0.toString() === 'a') undoManager.undo() t.assert(text0.toString() === '') text0.insert(0, 'abc') text1.insert(0, 'xyz') testConnector.syncAll() undoManager.undo() t.assert(text0.toString() === 'xyz') undoManager.redo() t.assert(text0.toString() === 'abcxyz') testConnector.syncAll() text1.delete(0, 1) testConnector.syncAll() undoManager.undo() t.assert(text0.toString() === 'xyz') undoManager.redo() t.assert(text0.toString() === 'bcxyz') // test marks text0.format(1, 3, { bold: true }) t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]) undoManager.undo() t.compare(text0.toDelta(), [{ insert: 'bcxyz' }]) undoManager.redo() t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]) } /** * Test case to fix #241 * @param {t.TestCase} _tc */ export const testEmptyTypeScope = _tc => { const ydoc = new Y.Doc() const um = new Y.UndoManager([], { doc: ydoc }) const yarray = ydoc.getArray() um.addToScope(yarray) yarray.insert(0, [1]) um.undo() t.assert(yarray.length === 0) } /** * Test case to fix #241 * @param {t.TestCase} _tc */ export const testDoubleUndo = _tc => { const doc = new Y.Doc() const text = doc.getText() text.insert(0, '1221') const manager = new Y.UndoManager(text) text.insert(2, '3') text.insert(3, '3') manager.undo() manager.undo() text.insert(2, '3') t.compareStrings(text.toString(), '12321') } /** * @param {t.TestCase} tc */ export const testUndoMap = tc => { const { testConnector, map0, map1 } = init(tc, { users: 2 }) map0.set('a', 0) const undoManager = new Y.UndoManager(map0) map0.set('a', 1) undoManager.undo() t.assert(map0.get('a') === 0) undoManager.redo() t.assert(map0.get('a') === 1) // testing sub-types and if it can restore a whole type const subType = new Y.Map() map0.set('a', subType) subType.set('x', 42) t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } })) undoManager.undo() t.assert(map0.get('a') === 1) undoManager.redo() t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } })) testConnector.syncAll() // if content is overwritten by another user, undo operations should be skipped map1.set('a', 44) testConnector.syncAll() undoManager.undo() t.assert(map0.get('a') === 44) undoManager.redo() t.assert(map0.get('a') === 44) // test setting value multiple times map0.set('b', 'initial') undoManager.stopCapturing() map0.set('b', 'val1') map0.set('b', 'val2') undoManager.stopCapturing() undoManager.undo() t.assert(map0.get('b') === 'initial') } /** * @param {t.TestCase} tc */ export const testUndoArray = tc => { const { testConnector, array0, array1 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(array0) array0.insert(0, [1, 2, 3]) array1.insert(0, [4, 5, 6]) testConnector.syncAll() t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6]) undoManager.undo() t.compare(array0.toArray(), [4, 5, 6]) undoManager.redo() t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6]) testConnector.syncAll() array1.delete(0, 1) // user1 deletes [1] testConnector.syncAll() undoManager.undo() t.compare(array0.toArray(), [4, 5, 6]) undoManager.redo() t.compare(array0.toArray(), [2, 3, 4, 5, 6]) array0.delete(0, 5) // test nested structure const ymap = new Y.Map() array0.insert(0, [ymap]) t.compare(array0.toJSON(), [{}]) undoManager.stopCapturing() ymap.set('a', 1) t.compare(array0.toJSON(), [{ a: 1 }]) undoManager.undo() t.compare(array0.toJSON(), [{}]) undoManager.undo() t.compare(array0.toJSON(), [2, 3, 4, 5, 6]) undoManager.redo() t.compare(array0.toJSON(), [{}]) undoManager.redo() t.compare(array0.toJSON(), [{ a: 1 }]) testConnector.syncAll() array1.get(0).set('b', 2) testConnector.syncAll() t.compare(array0.toJSON(), [{ a: 1, b: 2 }]) undoManager.undo() t.compare(array0.toJSON(), [{ b: 2 }]) undoManager.undo() t.compare(array0.toJSON(), [2, 3, 4, 5, 6]) undoManager.redo() t.compare(array0.toJSON(), [{ b: 2 }]) undoManager.redo() t.compare(array0.toJSON(), [{ a: 1, b: 2 }]) } /** * @param {t.TestCase} tc */ export const testUndoXml = tc => { const { xml0 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(xml0) const child = new Y.XmlElement('p') xml0.insert(0, [child]) const textchild = new Y.XmlText('content') child.insert(0, [textchild]) t.assert(xml0.toString() === '

content

') // format textchild and revert that change undoManager.stopCapturing() textchild.format(3, 4, { bold: {} }) t.assert(xml0.toString() === '

content

') undoManager.undo() t.assert(xml0.toString() === '

content

') undoManager.redo() t.assert(xml0.toString() === '

content

') xml0.delete(0, 1) t.assert(xml0.toString() === '') undoManager.undo() t.assert(xml0.toString() === '

content

') } /** * @param {t.TestCase} tc */ export const testUndoEvents = tc => { const { text0 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(text0) let counter = 0 let receivedMetadata = -1 undoManager.on('stack-item-added', /** @param {any} event */ event => { t.assert(event.type != null) t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0)) event.stackItem.meta.set('test', counter++) }) undoManager.on('stack-item-popped', /** @param {any} event */ event => { t.assert(event.type != null) t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0)) receivedMetadata = event.stackItem.meta.get('test') }) text0.insert(0, 'abc') undoManager.undo() t.assert(receivedMetadata === 0) undoManager.redo() t.assert(receivedMetadata === 1) } /** * @param {t.TestCase} tc */ export const testTrackClass = tc => { const { users, text0 } = init(tc, { users: 3 }) // only track origins that are numbers const undoManager = new Y.UndoManager(text0, { trackedOrigins: new Set([Number]) }) users[0].transact(() => { text0.insert(0, 'abc') }, 42) t.assert(text0.toString() === 'abc') undoManager.undo() t.assert(text0.toString() === '') } /** * @param {t.TestCase} tc */ export const testTypeScope = tc => { const { array0 } = init(tc, { users: 3 }) // only track origins that are numbers const text0 = new Y.Text() const text1 = new Y.Text() array0.insert(0, [text0, text1]) const undoManager = new Y.UndoManager(text0) const undoManagerBoth = new Y.UndoManager([text0, text1]) text1.insert(0, 'abc') t.assert(undoManager.undoStack.length === 0) t.assert(undoManagerBoth.undoStack.length === 1) t.assert(text1.toString() === 'abc') undoManager.undo() t.assert(text1.toString() === 'abc') undoManagerBoth.undo() t.assert(text1.toString() === '') } /** * @param {t.TestCase} tc */ export const testUndoInEmbed = tc => { const { text0 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(text0) const nestedText = new Y.Text('initial text') undoManager.stopCapturing() text0.insertEmbed(0, nestedText, { bold: true }) t.assert(nestedText.toString() === 'initial text') undoManager.stopCapturing() nestedText.delete(0, nestedText.length) nestedText.insert(0, 'other text') t.assert(nestedText.toString() === 'other text') undoManager.undo() t.assert(nestedText.toString() === 'initial text') undoManager.undo() t.assert(text0.length === 0) } /** * @param {t.TestCase} tc */ export const testUndoDeleteFilter = tc => { /** * @type {Y.Array} */ const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0) const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) }) const map0 = new Y.Map() map0.set('hi', 1) const map1 = new Y.Map() array0.insert(0, [map0, map1]) undoManager.undo() t.assert(array0.length === 1) array0.get(0) t.assert(Array.from(array0.get(0).keys()).length === 1) } /** * This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6 * @param {t.TestCase} _tc */ export const testUndoUntilChangePerformed = _tc => { const doc = new Y.Doc() const doc2 = new Y.Doc() doc.on('update', update => Y.applyUpdate(doc2, update)) doc2.on('update', update => Y.applyUpdate(doc, update)) const yArray = doc.getArray('array') const yArray2 = doc2.getArray('array') const yMap = new Y.Map() yMap.set('hello', 'world') yArray.push([yMap]) const yMap2 = new Y.Map() yMap2.set('key', 'value') yArray.push([yMap2]) const undoManager = new Y.UndoManager([yArray], { trackedOrigins: new Set([doc.clientID]) }) const undoManager2 = new Y.UndoManager([doc2.get('array')], { trackedOrigins: new Set([doc2.clientID]) }) Y.transact(doc, () => yMap2.set('key', 'value modified'), doc.clientID) undoManager.stopCapturing() Y.transact(doc, () => yMap.set('hello', 'world modified'), doc.clientID) Y.transact(doc2, () => yArray2.delete(0), doc2.clientID) undoManager2.undo() undoManager.undo() t.compareStrings(yMap2.get('key'), 'value') } /** * This issue has been reported in https://github.com/yjs/yjs/issues/317 * @param {t.TestCase} _tc */ export const testUndoNestedUndoIssue = _tc => { const doc = new Y.Doc({ gc: false }) const design = doc.getMap() const undoManager = new Y.UndoManager(design, { captureTimeout: 0 }) /** * @type {Y.Map} */ const text = new Y.Map() const blocks1 = new Y.Array() const blocks1block = new Y.Map() doc.transact(() => { blocks1block.set('text', 'Type Something') blocks1.push([blocks1block]) text.set('blocks', blocks1block) design.set('text', text) }) const blocks2 = new Y.Array() const blocks2block = new Y.Map() doc.transact(() => { blocks2block.set('text', 'Something') blocks2.push([blocks2block]) text.set('blocks', blocks2block) }) const blocks3 = new Y.Array() const blocks3block = new Y.Map() doc.transact(() => { blocks3block.set('text', 'Something Else') blocks3.push([blocks3block]) text.set('blocks', blocks3block) }) t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } }) undoManager.undo() t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } }) undoManager.undo() t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } }) undoManager.undo() t.compare(design.toJSON(), { }) undoManager.redo() t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } }) undoManager.redo() t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } }) undoManager.redo() t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } }) } /** * This issue has been reported in https://github.com/yjs/yjs/issues/355 * * @param {t.TestCase} _tc */ export const testConsecutiveRedoBug = _tc => { const doc = new Y.Doc() const yRoot = doc.getMap() const undoMgr = new Y.UndoManager(yRoot) let yPoint = new Y.Map() yPoint.set('x', 0) yPoint.set('y', 0) yRoot.set('a', yPoint) undoMgr.stopCapturing() yPoint.set('x', 100) yPoint.set('y', 100) undoMgr.stopCapturing() yPoint.set('x', 200) yPoint.set('y', 200) undoMgr.stopCapturing() yPoint.set('x', 300) yPoint.set('y', 300) undoMgr.stopCapturing() t.compare(yPoint.toJSON(), { x: 300, y: 300 }) undoMgr.undo() // x=200, y=200 t.compare(yPoint.toJSON(), { x: 200, y: 200 }) undoMgr.undo() // x=100, y=100 t.compare(yPoint.toJSON(), { x: 100, y: 100 }) undoMgr.undo() // x=0, y=0 t.compare(yPoint.toJSON(), { x: 0, y: 0 }) undoMgr.undo() // nil t.compare(yRoot.get('a'), undefined) undoMgr.redo() // x=0, y=0 yPoint = yRoot.get('a') t.compare(yPoint.toJSON(), { x: 0, y: 0 }) undoMgr.redo() // x=100, y=100 t.compare(yPoint.toJSON(), { x: 100, y: 100 }) undoMgr.redo() // x=200, y=200 t.compare(yPoint.toJSON(), { x: 200, y: 200 }) undoMgr.redo() // expected x=300, y=300, actually nil t.compare(yPoint.toJSON(), { x: 300, y: 300 }) } /** * This issue has been reported in https://github.com/yjs/yjs/issues/304 * * @param {t.TestCase} _tc */ export const testUndoXmlBug = _tc => { const origin = 'origin' const doc = new Y.Doc() const fragment = doc.getXmlFragment('t') const undoManager = new Y.UndoManager(fragment, { captureTimeout: 0, trackedOrigins: new Set([origin]) }) // create element doc.transact(() => { const e = new Y.XmlElement('test-node') e.setAttribute('a', '100') e.setAttribute('b', '0') fragment.insert(fragment.length, [e]) }, origin) // change one attribute doc.transact(() => { const e = fragment.get(0) e.setAttribute('a', '200') }, origin) // change both attributes doc.transact(() => { const e = fragment.get(0) e.setAttribute('a', '180') e.setAttribute('b', '50') }, origin) undoManager.undo() undoManager.undo() undoManager.undo() undoManager.redo() undoManager.redo() undoManager.redo() t.compare(fragment.toString(), '') } /** * This issue has been reported in https://github.com/yjs/yjs/issues/343 * * @param {t.TestCase} _tc */ export const testUndoBlockBug = _tc => { const doc = new Y.Doc({ gc: false }) const design = doc.getMap() const undoManager = new Y.UndoManager(design, { captureTimeout: 0 }) const text = new Y.Map() const blocks1 = new Y.Array() const blocks1block = new Y.Map() doc.transact(() => { blocks1block.set('text', '1') blocks1.push([blocks1block]) text.set('blocks', blocks1block) design.set('text', text) }) const blocks2 = new Y.Array() const blocks2block = new Y.Map() doc.transact(() => { blocks2block.set('text', '2') blocks2.push([blocks2block]) text.set('blocks', blocks2block) }) const blocks3 = new Y.Array() const blocks3block = new Y.Map() doc.transact(() => { blocks3block.set('text', '3') blocks3.push([blocks3block]) text.set('blocks', blocks3block) }) const blocks4 = new Y.Array() const blocks4block = new Y.Map() doc.transact(() => { blocks4block.set('text', '4') blocks4.push([blocks4block]) text.set('blocks', blocks4block) }) // {"text":{"blocks":{"text":"4"}}} undoManager.undo() // {"text":{"blocks":{"3"}}} undoManager.undo() // {"text":{"blocks":{"text":"2"}}} undoManager.undo() // {"text":{"blocks":{"text":"1"}}} undoManager.undo() // {} undoManager.redo() // {"text":{"blocks":{"text":"1"}}} undoManager.redo() // {"text":{"blocks":{"text":"2"}}} undoManager.redo() // {"text":{"blocks":{"text":"3"}}} undoManager.redo() // {"text":{}} t.compare(design.toJSON(), { text: { blocks: { text: '4' } } }) } /** * Undo text formatting delete should not corrupt peer state. * * @see https://github.com/yjs/yjs/issues/392 * @param {t.TestCase} _tc */ export const testUndoDeleteTextFormat = _tc => { const doc = new Y.Doc() const text = doc.getText() text.insert(0, 'Attack ships on fire off the shoulder of Orion.') const doc2 = new Y.Doc() const text2 = doc2.getText() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) const undoManager = new Y.UndoManager(text) text.format(13, 7, { bold: true }) undoManager.stopCapturing() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) text.format(16, 4, { bold: null }) undoManager.stopCapturing() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) undoManager.undo() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) const expect = [ { insert: 'Attack ships ' }, { insert: 'on fire', attributes: { bold: true } }, { insert: ' off the shoulder of Orion.' } ] t.compare(text.toDelta(), expect) t.compare(text2.toDelta(), expect) } /** * Undo text formatting delete should not corrupt peer state. * * @see https://github.com/yjs/yjs/issues/392 * @param {t.TestCase} _tc */ export const testBehaviorOfIgnoreremotemapchangesProperty = _tc => { const doc = new Y.Doc() const doc2 = new Y.Doc() doc.on('update', update => Y.applyUpdate(doc2, update, doc)) doc2.on('update', update => Y.applyUpdate(doc, update, doc2)) const map1 = doc.getMap() const map2 = doc2.getMap() const um1 = new Y.UndoManager(map1, { ignoreRemoteMapChanges: true }) map1.set('x', 1) map2.set('x', 2) map1.set('x', 3) map2.set('x', 4) um1.undo() t.assert(map1.get('x') === 2) t.assert(map2.get('x') === 2) } /** * Special deletion case. * * @see https://github.com/yjs/yjs/issues/447 * @param {t.TestCase} _tc */ export const testSpecialDeletionCase = _tc => { const origin = 'undoable' const doc = new Y.Doc() const fragment = doc.getXmlFragment() const undoManager = new Y.UndoManager(fragment, { trackedOrigins: new Set([origin]) }) doc.transact(() => { const e = new Y.XmlElement('test') e.setAttribute('a', '1') e.setAttribute('b', '2') fragment.insert(0, [e]) }) t.compareStrings(fragment.toString(), '') doc.transact(() => { // change attribute "b" and delete test-node const e = fragment.get(0) e.setAttribute('b', '3') fragment.delete(0) }, origin) t.compareStrings(fragment.toString(), '') undoManager.undo() t.compareStrings(fragment.toString(), '') } /** * Deleted entries in a map should be restored on undo. * * @see https://github.com/yjs/yjs/issues/500 * @param {t.TestCase} tc */ export const testUndoDeleteInMap = (tc) => { const { map0 } = init(tc, { users: 3 }) const undoManager = new Y.UndoManager(map0, { captureTimeout: 0 }) map0.set('a', 'a') map0.delete('a') map0.set('a', 'b') map0.delete('a') map0.set('a', 'c') map0.delete('a') map0.set('a', 'd') t.compare(map0.toJSON(), { a: 'd' }) undoManager.undo() t.compare(map0.toJSON(), {}) undoManager.undo() t.compare(map0.toJSON(), { a: 'c' }) undoManager.undo() t.compare(map0.toJSON(), {}) undoManager.undo() t.compare(map0.toJSON(), { a: 'b' }) undoManager.undo() t.compare(map0.toJSON(), {}) undoManager.undo() t.compare(map0.toJSON(), { a: 'a' }) } yjs-13.6.8/tests/updates.tests.js000066400000000000000000000263411450200430400167220ustar00rootroot00000000000000import * as t from 'lib0/testing' import { init, compare } from './testHelper.js' // eslint-disable-line import * as Y from '../src/index.js' import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js' import * as encoding from 'lib0/encoding' import * as decoding from 'lib0/decoding' import * as object from 'lib0/object' /** * @typedef {Object} Enc * @property {function(Array):Uint8Array} Enc.mergeUpdates * @property {function(Y.Doc):Uint8Array} Enc.encodeStateAsUpdate * @property {function(Y.Doc, Uint8Array):void} Enc.applyUpdate * @property {function(Uint8Array):void} Enc.logUpdate * @property {function(Uint8Array):{from:Map,to:Map}} Enc.parseUpdateMeta * @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate * @property {string} Enc.updateEventName * @property {string} Enc.description * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate */ /** * @type {Enc} */ const encV1 = { mergeUpdates: Y.mergeUpdates, encodeStateAsUpdate: Y.encodeStateAsUpdate, applyUpdate: Y.applyUpdate, logUpdate: Y.logUpdate, parseUpdateMeta: Y.parseUpdateMeta, encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdate, encodeStateVector: Y.encodeStateVector, updateEventName: 'update', description: 'V1', diffUpdate: Y.diffUpdate } /** * @type {Enc} */ const encV2 = { mergeUpdates: Y.mergeUpdatesV2, encodeStateAsUpdate: Y.encodeStateAsUpdateV2, applyUpdate: Y.applyUpdateV2, logUpdate: Y.logUpdateV2, parseUpdateMeta: Y.parseUpdateMetaV2, encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2, encodeStateVector: Y.encodeStateVector, updateEventName: 'updateV2', description: 'V2', diffUpdate: Y.diffUpdateV2 } /** * @type {Enc} */ const encDoc = { mergeUpdates: (updates) => { const ydoc = new Y.Doc({ gc: false }) updates.forEach(update => { Y.applyUpdateV2(ydoc, update) }) return Y.encodeStateAsUpdateV2(ydoc) }, encodeStateAsUpdate: Y.encodeStateAsUpdateV2, applyUpdate: Y.applyUpdateV2, logUpdate: Y.logUpdateV2, parseUpdateMeta: Y.parseUpdateMetaV2, encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2, encodeStateVector: Y.encodeStateVector, updateEventName: 'updateV2', description: 'Merge via Y.Doc', /** * @param {Uint8Array} update * @param {Uint8Array} sv */ diffUpdate: (update, sv) => { const ydoc = new Y.Doc({ gc: false }) Y.applyUpdateV2(ydoc, update) return Y.encodeStateAsUpdateV2(ydoc, sv) } } const encoders = [encV1, encV2, encDoc] /** * @param {Array} users * @param {Enc} enc */ const fromUpdates = (users, enc) => { const updates = users.map(user => enc.encodeStateAsUpdate(user) ) const ydoc = new Y.Doc() enc.applyUpdate(ydoc, enc.mergeUpdates(updates)) return ydoc } /** * @param {t.TestCase} tc */ export const testMergeUpdates = tc => { const { users, array0, array1 } = init(tc, { users: 3 }) array0.insert(0, [1]) array1.insert(0, [2]) compare(users) encoders.forEach(enc => { const merged = fromUpdates(users, enc) t.compareArrays(array0.toArray(), merged.getArray('array').toArray()) }) } /** * @param {t.TestCase} tc */ export const testKeyEncoding = tc => { const { users, text0, text1 } = init(tc, { users: 2 }) text0.insert(0, 'a', { italic: true }) text0.insert(0, 'b') text0.insert(0, 'c', { italic: true }) const update = Y.encodeStateAsUpdateV2(users[0]) Y.applyUpdateV2(users[1], update) t.compare(text1.toDelta(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }]) compare(users) } /** * @param {Y.Doc} ydoc * @param {Array} updates - expecting at least 4 updates * @param {Enc} enc * @param {boolean} hasDeletes */ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => { const cases = [] // Case 1: Simple case, simply merge everything cases.push(enc.mergeUpdates(updates)) // Case 2: Overlapping updates cases.push(enc.mergeUpdates([ enc.mergeUpdates(updates.slice(2)), enc.mergeUpdates(updates.slice(0, 2)) ])) // Case 3: Overlapping updates cases.push(enc.mergeUpdates([ enc.mergeUpdates(updates.slice(2)), enc.mergeUpdates(updates.slice(1, 3)), updates[0] ])) // Case 4: Separated updates (containing skips) cases.push(enc.mergeUpdates([ enc.mergeUpdates([updates[0], updates[2]]), enc.mergeUpdates([updates[1], updates[3]]), enc.mergeUpdates(updates.slice(4)) ])) // Case 5: overlapping with many duplicates cases.push(enc.mergeUpdates(cases)) // const targetState = enc.encodeStateAsUpdate(ydoc) // t.info('Target State: ') // enc.logUpdate(targetState) cases.forEach((mergedUpdates, i) => { // t.info('State Case $' + i + ':') // enc.logUpdate(updates) const merged = new Y.Doc({ gc: false }) enc.applyUpdate(merged, mergedUpdates) t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray()) t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates)) if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates? for (let j = 1; j < updates.length; j++) { const partMerged = enc.mergeUpdates(updates.slice(j)) const partMeta = enc.parseUpdateMeta(partMerged) const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j))) const diffed = enc.diffUpdate(mergedUpdates, targetSV) const diffedMeta = enc.parseUpdateMeta(diffed) t.compare(partMeta, diffedMeta) { // We can'd do the following // - t.compare(diffed, mergedDeletes) // because diffed contains the set of all deletes. // So we add all deletes from `diffed` to `partDeletes` and compare then const decoder = decoding.createDecoder(diffed) const updateDecoder = new UpdateDecoderV2(decoder) readClientsStructRefs(updateDecoder, new Y.Doc()) const ds = readDeleteSet(updateDecoder) const updateEncoder = new UpdateEncoderV2() encoding.writeVarUint(updateEncoder.restEncoder, 0) // 0 structs writeDeleteSet(updateEncoder, ds) const deletesUpdate = updateEncoder.toUint8Array() const mergedDeletes = Y.mergeUpdatesV2([deletesUpdate, partMerged]) if (!hasDeletes || enc !== encDoc) { // deletes will almost definitely lead to different encoders because of the mergeStruct feature that is present in encDoc t.compare(diffed, mergedDeletes) } } } } const meta = enc.parseUpdateMeta(mergedUpdates) meta.from.forEach((clock, client) => t.assert(clock === 0)) meta.to.forEach((clock, client) => { const structs = /** @type {Array} */ (merged.store.clients.get(client)) const lastStruct = structs[structs.length - 1] t.assert(lastStruct.id.clock + lastStruct.length === clock) }) }) } /** * @param {t.TestCase} tc */ export const testMergeUpdates1 = tc => { encoders.forEach((enc, i) => { t.info(`Using encoder: ${enc.description}`) const ydoc = new Y.Doc({ gc: false }) const updates = /** @type {Array} */ ([]) ydoc.on(enc.updateEventName, update => { updates.push(update) }) const array = ydoc.getArray() array.insert(0, [1]) array.insert(0, [2]) array.insert(0, [3]) array.insert(0, [4]) checkUpdateCases(ydoc, updates, enc, false) }) } /** * @param {t.TestCase} tc */ export const testMergeUpdates2 = tc => { encoders.forEach((enc, i) => { t.info(`Using encoder: ${enc.description}`) const ydoc = new Y.Doc({ gc: false }) const updates = /** @type {Array} */ ([]) ydoc.on(enc.updateEventName, update => { updates.push(update) }) const array = ydoc.getArray() array.insert(0, [1, 2]) array.delete(1, 1) array.insert(0, [3, 4]) array.delete(1, 2) checkUpdateCases(ydoc, updates, enc, true) }) } /** * @param {t.TestCase} tc */ export const testMergePendingUpdates = tc => { const yDoc = new Y.Doc() /** * @type {Array} */ const serverUpdates = [] yDoc.on('update', (update, origin, c) => { serverUpdates.splice(serverUpdates.length, 0, update) }) const yText = yDoc.getText('textBlock') yText.applyDelta([{ insert: 'r' }]) yText.applyDelta([{ insert: 'o' }]) yText.applyDelta([{ insert: 'n' }]) yText.applyDelta([{ insert: 'e' }]) yText.applyDelta([{ insert: 'n' }]) const yDoc1 = new Y.Doc() Y.applyUpdate(yDoc1, serverUpdates[0]) const update1 = Y.encodeStateAsUpdate(yDoc1) const yDoc2 = new Y.Doc() Y.applyUpdate(yDoc2, update1) Y.applyUpdate(yDoc2, serverUpdates[1]) const update2 = Y.encodeStateAsUpdate(yDoc2) const yDoc3 = new Y.Doc() Y.applyUpdate(yDoc3, update2) Y.applyUpdate(yDoc3, serverUpdates[3]) const update3 = Y.encodeStateAsUpdate(yDoc3) const yDoc4 = new Y.Doc() Y.applyUpdate(yDoc4, update3) Y.applyUpdate(yDoc4, serverUpdates[2]) const update4 = Y.encodeStateAsUpdate(yDoc4) const yDoc5 = new Y.Doc() Y.applyUpdate(yDoc5, update4) Y.applyUpdate(yDoc5, serverUpdates[4]) // @ts-ignore const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line const yText5 = yDoc5.getText('textBlock') t.compareStrings(yText5.toString(), 'nenor') } /** * @param {t.TestCase} tc */ export const testObfuscateUpdates = tc => { const ydoc = new Y.Doc() const ytext = ydoc.getText('text') const ymap = ydoc.getMap('map') const yarray = ydoc.getArray('array') // test ytext ytext.applyDelta([{ insert: 'text', attributes: { bold: true } }, { insert: { href: 'supersecreturl' } }]) // test ymap ymap.set('key', 'secret1') ymap.set('key', 'secret2') // test yarray with subtype & subdoc const subtype = new Y.XmlElement('secretnodename') const subdoc = new Y.Doc({ guid: 'secret' }) subtype.setAttribute('attr', 'val') yarray.insert(0, ['teststring', 42, subtype, subdoc]) // obfuscate the content and put it into a new document const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc)) const odoc = new Y.Doc() Y.applyUpdate(odoc, obfuscatedUpdate) const otext = odoc.getText('text') const omap = odoc.getMap('map') const oarray = odoc.getArray('array') // test ytext const delta = otext.toDelta() t.assert(delta.length === 2) t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4) t.assert(object.length(delta[0].attributes) === 1) t.assert(!object.hasProperty(delta[0].attributes, 'bold')) t.assert(object.length(delta[1]) === 1) t.assert(object.hasProperty(delta[1], 'insert')) // test ymap t.assert(omap.size === 1) t.assert(!omap.has('key')) // test yarray with subtype & subdoc const result = oarray.toArray() t.assert(result.length === 4) t.assert(result[0] !== 'teststring') t.assert(result[1] !== 42) const osubtype = /** @type {Y.XmlElement} */ (result[2]) const osubdoc = result[3] // test subtype t.assert(osubtype.nodeName !== subtype.nodeName) t.assert(object.length(osubtype.getAttributes()) === 1) t.assert(osubtype.getAttribute('attr') === undefined) // test subdoc t.assert(osubdoc.guid !== subdoc.guid) } yjs-13.6.8/tests/y-array.tests.js000066400000000000000000000376361450200430400166520ustar00rootroot00000000000000import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' import * as math from 'lib0/math' /** * @param {t.TestCase} tc */ export const testBasicUpdate = tc => { const doc1 = new Y.Doc() const doc2 = new Y.Doc() doc1.getArray('array').insert(0, ['hi']) const update = Y.encodeStateAsUpdate(doc1) Y.applyUpdate(doc2, update) t.compare(doc2.getArray('array').toArray(), ['hi']) } /** * @param {t.TestCase} tc */ export const testSlice = tc => { const doc1 = new Y.Doc() const arr = doc1.getArray('array') arr.insert(0, [1, 2, 3]) t.compareArrays(arr.slice(0), [1, 2, 3]) t.compareArrays(arr.slice(1), [2, 3]) t.compareArrays(arr.slice(0, -1), [1, 2]) arr.insert(0, [0]) t.compareArrays(arr.slice(0), [0, 1, 2, 3]) t.compareArrays(arr.slice(0, 2), [0, 1]) } /** * @param {t.TestCase} tc */ export const testArrayFrom = tc => { const doc1 = new Y.Doc() const db1 = doc1.getMap('root') const nestedArray1 = Y.Array.from([0, 1, 2]) db1.set('array', nestedArray1) t.compare(nestedArray1.toArray(), [0, 1, 2]) } /** * Debugging yjs#297 - a critical bug connected to the search-marker approach * * @param {t.TestCase} tc */ export const testLengthIssue = tc => { const doc1 = new Y.Doc() const arr = doc1.getArray('array') arr.push([0, 1, 2, 3]) arr.delete(0) arr.insert(0, [0]) t.assert(arr.length === arr.toArray().length) doc1.transact(() => { arr.delete(1) t.assert(arr.length === arr.toArray().length) arr.insert(1, [1]) t.assert(arr.length === arr.toArray().length) arr.delete(2) t.assert(arr.length === arr.toArray().length) arr.insert(2, [2]) t.assert(arr.length === arr.toArray().length) }) t.assert(arr.length === arr.toArray().length) arr.delete(1) t.assert(arr.length === arr.toArray().length) arr.insert(1, [1]) t.assert(arr.length === arr.toArray().length) } /** * Debugging yjs#314 * * @param {t.TestCase} tc */ export const testLengthIssue2 = tc => { const doc = new Y.Doc() const next = doc.getArray() doc.transact(() => { next.insert(0, ['group2']) }) doc.transact(() => { next.insert(1, ['rectangle3']) }) doc.transact(() => { next.delete(0) next.insert(0, ['rectangle3']) }) next.delete(1) doc.transact(() => { next.insert(1, ['ellipse4']) }) doc.transact(() => { next.insert(2, ['ellipse3']) }) doc.transact(() => { next.insert(3, ['ellipse2']) }) doc.transact(() => { doc.transact(() => { t.fails(() => { next.insert(5, ['rectangle2']) }) next.insert(4, ['rectangle2']) }) doc.transact(() => { // this should not throw an error message next.delete(4) }) }) console.log(next.toArray()) } /** * @param {t.TestCase} tc */ export const testDeleteInsert = tc => { const { users, array0 } = init(tc, { users: 2 }) array0.delete(0, 0) t.describe('Does not throw when deleting zero elements with position 0') t.fails(() => { array0.delete(1, 1) }) array0.insert(0, ['A']) array0.delete(1, 0) t.describe('Does not throw when deleting zero elements with valid position 1') compare(users) } /** * @param {t.TestCase} tc */ export const testInsertThreeElementsTryRegetProperty = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, true, false]) t.compare(array0.toJSON(), [1, true, false], '.toJSON() works') testConnector.flushAllMessages() t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync') compare(users) } /** * @param {t.TestCase} tc */ export const testConcurrentInsertWithThreeConflicts = tc => { const { users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, [0]) array1.insert(0, [1]) array2.insert(0, [2]) compare(users) } /** * @param {t.TestCase} tc */ export const testConcurrentInsertDeleteWithThreeConflicts = tc => { const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y', 'z']) testConnector.flushAllMessages() array0.insert(1, [0]) array1.delete(0) array1.delete(1, 1) array2.insert(1, [2]) compare(users) } /** * @param {t.TestCase} tc */ export const testInsertionsInLateSync = tc => { const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y']) testConnector.flushAllMessages() users[1].disconnect() users[2].disconnect() array0.insert(1, ['user0']) array1.insert(1, ['user1']) array2.insert(1, ['user2']) users[1].connect() users[2].connect() testConnector.flushAllMessages() compare(users) } /** * @param {t.TestCase} tc */ export const testDisconnectReallyPreventsSendingMessages = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y']) testConnector.flushAllMessages() users[1].disconnect() users[2].disconnect() array0.insert(1, ['user0']) array1.insert(1, ['user1']) t.compare(array0.toJSON(), ['x', 'user0', 'y']) t.compare(array1.toJSON(), ['x', 'user1', 'y']) users[1].connect() users[2].connect() compare(users) } /** * @param {t.TestCase} tc */ export const testDeletionsInLateSync = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['x', 'y']) testConnector.flushAllMessages() users[1].disconnect() array1.delete(1, 1) array0.delete(0, 2) users[1].connect() compare(users) } /** * @param {t.TestCase} tc */ export const testInsertThenMergeDeleteOnSync = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['x', 'y', 'z']) testConnector.flushAllMessages() users[0].disconnect() array1.delete(0, 3) users[0].connect() compare(users) } /** * @param {t.TestCase} tc */ export const testInsertAndDeleteEvents = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {Object?} */ let event = null array0.observe(e => { event = e }) array0.insert(0, [0, 1, 2]) t.assert(event !== null) event = null array0.delete(0) t.assert(event !== null) event = null array0.delete(0, 2) t.assert(event !== null) event = null compare(users) } /** * @param {t.TestCase} tc */ export const testNestedObserverEvents = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {Array} */ const vals = [] array0.observe(e => { if (array0.length === 1) { // inserting, will call this observer again // we expect that this observer is called after this event handler finishedn array0.insert(1, [1]) vals.push(0) } else { // this should be called the second time an element is inserted (above case) vals.push(1) } }) array0.insert(0, [0]) t.compareArrays(vals, [0, 1]) t.compareArrays(array0.toArray(), [0, 1]) compare(users) } /** * @param {t.TestCase} tc */ export const testInsertAndDeleteEventsForTypes = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {Object|null} */ let event = null array0.observe(e => { event = e }) array0.insert(0, [new Y.Array()]) t.assert(event !== null) event = null array0.delete(0) t.assert(event !== null) event = null compare(users) } /** * This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2 * * Deep observers generate multiple events. When an array added at item at, say, position 0, * and item 1 changed then the array-add event should fire first so that the change event * path is correct. A array binding might lead to an inconsistent state otherwise. * * @param {t.TestCase} tc */ export const testObserveDeepEventOrder = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {Array} */ let events = [] array0.observeDeep(e => { events = e }) array0.insert(0, [new Y.Map()]) users[0].transact(() => { array0.get(0).set('a', 'a') array0.insert(0, [0]) }) for (let i = 1; i < events.length; i++) { t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first') } } /** * @param {t.TestCase} tc */ export const testChangeEvent = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {any} */ let changes = null array0.observe(e => { changes = e.changes }) const newArr = new Y.Array() array0.insert(0, [newArr, 4, 'dtrn']) t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0) t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }]) changes = null array0.delete(0, 2) t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2) t.compare(changes.delta, [{ delete: 2 }]) changes = null array0.insert(1, [0.1]) t.assert(changes !== null && changes.added.size === 1 && changes.deleted.size === 0) t.compare(changes.delta, [{ retain: 1 }, { insert: [0.1] }]) compare(users) } /** * @param {t.TestCase} tc */ export const testInsertAndDeleteEventsForTypes2 = tc => { const { array0, users } = init(tc, { users: 2 }) /** * @type {Array>} */ const events = [] array0.observe(e => { events.push(e) }) array0.insert(0, ['hi', new Y.Map()]) t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements') array0.delete(1) t.assert(events.length === 2, 'Event is triggered exactly once for deletion') compare(users) } /** * This issue has been reported here https://github.com/yjs/yjs/issues/155 * @param {t.TestCase} tc */ export const testNewChildDoesNotEmitEventInTransaction = tc => { const { array0, users } = init(tc, { users: 2 }) let fired = false users[0].transact(() => { const newMap = new Y.Map() newMap.observe(() => { fired = true }) array0.insert(0, [newMap]) newMap.set('tst', 42) }) t.assert(!fired, 'Event does not trigger') } /** * @param {t.TestCase} tc */ export const testGarbageCollector = tc => { const { testConnector, users, array0 } = init(tc, { users: 3 }) array0.insert(0, ['x', 'y', 'z']) testConnector.flushAllMessages() users[0].disconnect() array0.delete(0, 3) users[0].connect() testConnector.flushAllMessages() compare(users) } /** * @param {t.TestCase} tc */ export const testEventTargetIsSetCorrectlyOnLocal = tc => { const { array0, users } = init(tc, { users: 3 }) /** * @type {any} */ let event array0.observe(e => { event = e }) array0.insert(0, ['stuff']) t.assert(event.target === array0, '"target" property is set correctly') compare(users) } /** * @param {t.TestCase} tc */ export const testEventTargetIsSetCorrectlyOnRemote = tc => { const { testConnector, array0, array1, users } = init(tc, { users: 3 }) /** * @type {any} */ let event array0.observe(e => { event = e }) array1.insert(0, ['stuff']) testConnector.flushAllMessages() t.assert(event.target === array0, '"target" property is set correctly') compare(users) } /** * @param {t.TestCase} tc */ export const testIteratingArrayContainingTypes = tc => { const y = new Y.Doc() const arr = y.getArray('arr') const numItems = 10 for (let i = 0; i < numItems; i++) { const map = new Y.Map() map.set('value', i) arr.push([map]) } let cnt = 0 for (const item of arr) { t.assert(item.get('value') === cnt++, 'value is correct') } y.destroy() } let _uniqueNumber = 0 const getUniqueNumber = () => _uniqueNumber++ /** * @type {Array} */ const arrayTransactions = [ function insert (user, gen) { const yarray = user.getArray('array') const uniqueNumber = getUniqueNumber() const content = [] const len = prng.int32(gen, 1, 4) for (let i = 0; i < len; i++) { content.push(uniqueNumber) } const pos = prng.int32(gen, 0, yarray.length) const oldContent = yarray.toArray() yarray.insert(pos, content) oldContent.splice(pos, 0, ...content) t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position }, function insertTypeArray (user, gen) { const yarray = user.getArray('array') const pos = prng.int32(gen, 0, yarray.length) yarray.insert(pos, [new Y.Array()]) const array2 = yarray.get(pos) array2.insert(0, [1, 2, 3, 4]) }, function insertTypeMap (user, gen) { const yarray = user.getArray('array') const pos = prng.int32(gen, 0, yarray.length) yarray.insert(pos, [new Y.Map()]) const map = yarray.get(pos) map.set('someprop', 42) map.set('someprop', 43) map.set('someprop', 44) }, function insertTypeNull (user, gen) { const yarray = user.getArray('array') const pos = prng.int32(gen, 0, yarray.length) yarray.insert(pos, [null]) }, function _delete (user, gen) { const yarray = user.getArray('array') const length = yarray.length if (length > 0) { let somePos = prng.int32(gen, 0, length - 1) let delLength = prng.int32(gen, 1, math.min(2, length - somePos)) if (prng.bool(gen)) { const type = yarray.get(somePos) if (type instanceof Y.Array && type.length > 0) { somePos = prng.int32(gen, 0, type.length - 1) delLength = prng.int32(gen, 0, math.min(2, type.length - somePos)) type.delete(somePos, delLength) } } else { const oldContent = yarray.toArray() yarray.delete(somePos, delLength) oldContent.splice(somePos, delLength) t.compareArrays(yarray.toArray(), oldContent) } } } ] /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests6 = tc => { applyRandomTests(tc, arrayTransactions, 6) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests40 = tc => { applyRandomTests(tc, arrayTransactions, 40) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests42 = tc => { applyRandomTests(tc, arrayTransactions, 42) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests43 = tc => { applyRandomTests(tc, arrayTransactions, 43) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests44 = tc => { applyRandomTests(tc, arrayTransactions, 44) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests45 = tc => { applyRandomTests(tc, arrayTransactions, 45) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests46 = tc => { applyRandomTests(tc, arrayTransactions, 46) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests300 = tc => { applyRandomTests(tc, arrayTransactions, 300) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests400 = tc => { applyRandomTests(tc, arrayTransactions, 400) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests500 = tc => { applyRandomTests(tc, arrayTransactions, 500) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests600 = tc => { applyRandomTests(tc, arrayTransactions, 600) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests1000 = tc => { applyRandomTests(tc, arrayTransactions, 1000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests1800 = tc => { applyRandomTests(tc, arrayTransactions, 1800) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests3000 = tc => { t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 3000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests5000 = tc => { t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 5000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYarrayTests30000 = tc => { t.skip(!t.production) applyRandomTests(tc, arrayTransactions, 30000) } yjs-13.6.8/tests/y-map.tests.js000066400000000000000000000454421450200430400163030ustar00rootroot00000000000000import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { compareIDs } from '../src/internals.js' import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' /** * Computing event changes after transaction should result in an error. See yjs#539 * * @param {t.TestCase} _tc */ export const testMapEventError = _tc => { const doc = new Y.Doc() const ymap = doc.getMap() /** * @type {any} */ let event = null ymap.observe((e) => { event = e }) t.fails(() => { t.info(event.keys) }) t.fails(() => { t.info(event.keys) }) } /** * @param {t.TestCase} tc */ export const testMapHavingIterableAsConstructorParamTests = tc => { const { map0 } = init(tc, { users: 1 }) const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' })) map0.set('m1', m1) t.assert(m1.get('number') === 1) t.assert(m1.get('string') === 'hello') const m2 = new Y.Map([ ['object', { x: 1 }], ['boolean', true] ]) map0.set('m2', m2) t.assert(m2.get('object').x === 1) t.assert(m2.get('boolean') === true) const m3 = new Y.Map([...m1, ...m2]) map0.set('m3', m3) t.assert(m3.get('number') === 1) t.assert(m3.get('string') === 'hello') t.assert(m3.get('object').x === 1) t.assert(m3.get('boolean') === true) } /** * @param {t.TestCase} tc */ export const testBasicMapTests = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) users[2].disconnect() map0.set('null', null) map0.set('number', 1) map0.set('string', 'hello Y') map0.set('object', { key: { key2: 'value' } }) map0.set('y-map', new Y.Map()) map0.set('boolean1', true) map0.set('boolean0', false) const map = map0.get('y-map') map.set('y-array', new Y.Array()) const array = map.get('y-array') array.insert(0, [0]) array.insert(0, [-1]) t.assert(map0.get('null') === null, 'client 0 computed the change (null)') t.assert(map0.get('number') === 1, 'client 0 computed the change (number)') t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)') t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)') t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)') t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') t.assert(map0.size === 7, 'client 0 map has correct size') users[2].connect() testConnector.flushAllMessages() t.assert(map1.get('null') === null, 'client 1 received the update (null)') t.assert(map1.get('number') === 1, 'client 1 received the update (number)') t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)') t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)') t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)') t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') t.assert(map1.size === 7, 'client 1 map has correct size') // compare disconnected user t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected') t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)') t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)') t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected') t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected') compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapProperty = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') map0.set('undefined', undefined) map0.set('null', null) t.compare(map0.get('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'stuffy') t.assert(u.get('undefined') === undefined, 'undefined') t.compare(u.get('null'), null, 'null') } compare(users) } /** * @param {t.TestCase} tc */ export const testYmapSetsYmap = tc => { const { users, map0 } = init(tc, { users: 2 }) const map = map0.set('Map', new Y.Map()) t.assert(map0.get('Map') === map) map.set('one', 1) t.compare(map.get('one'), 1) compare(users) } /** * @param {t.TestCase} tc */ export const testYmapSetsYarray = tc => { const { users, map0 } = init(tc, { users: 2 }) const array = map0.set('Array', new Y.Array()) t.assert(array === map0.get('Array')) array.insert(0, [1, 2, 3]) // @ts-ignore t.compare(map0.toJSON(), { Array: [1, 2, 3] }) compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertySyncs = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') t.compare(map0.get('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'stuffy') } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertyWithConflict = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'c1') } compare(users) } /** * @param {t.TestCase} tc */ export const testSizeAndDeleteOfMapProperty = tc => { const { map0 } = init(tc, { users: 1 }) map0.set('stuff', 'c0') map0.set('otherstuff', 'c1') t.assert(map0.size === 2, `map size is ${map0.size} expected 2`) map0.delete('stuff') t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`) map0.delete('otherstuff') t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`) } /** * @param {t.TestCase} tc */ export const testGetAndSetAndDeleteOfMapProperty = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.delete('stuff') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) } compare(users) } /** * @param {t.TestCase} tc */ export const testSetAndClearOfMapProperties = tc => { const { testConnector, users, map0 } = init(tc, { users: 1 }) map0.set('stuff', 'c0') map0.set('otherstuff', 'c1') map0.clear() testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) t.assert(u.get('otherstuff') === undefined) t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) } compare(users) } /** * @param {t.TestCase} tc */ export const testSetAndClearOfMapPropertiesWithConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() map0.set('otherstuff', 'c0') map1.set('otherstuff', 'c1') map2.set('otherstuff', 'c2') map3.set('otherstuff', 'c3') map3.clear() testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) t.assert(u.get('otherstuff') === undefined) t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'c3') } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() map0.set('stuff', 'deleteme') map1.set('stuff', 'c1') map2.set('stuff', 'c2') map3.set('stuff', 'c3') map3.delete('stuff') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) } compare(users) } /** * @param {t.TestCase} tc */ export const testObserveDeepProperties = tc => { const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 }) const _map1 = map1.set('map', new Y.Map()) let calls = 0 let dmapid map1.observeDeep(events => { events.forEach(event => { calls++ // @ts-ignore t.assert(event.keysChanged.has('deepmap')) t.assert(event.path.length === 1) t.assert(event.path[0] === 'map') // @ts-ignore dmapid = event.target.get('deepmap')._item.id }) }) testConnector.flushAllMessages() const _map3 = map3.get('map') _map3.set('deepmap', new Y.Map()) testConnector.flushAllMessages() const _map2 = map2.get('map') _map2.set('deepmap', new Y.Map()) testConnector.flushAllMessages() const dmap1 = _map1.get('deepmap') const dmap2 = _map2.get('deepmap') const dmap3 = _map3.get('deepmap') t.assert(calls > 0) t.assert(compareIDs(dmap1._item.id, dmap2._item.id)) t.assert(compareIDs(dmap1._item.id, dmap3._item.id)) // @ts-ignore we want the possibility of dmapid being undefined t.assert(compareIDs(dmap1._item.id, dmapid)) compare(users) } /** * @param {t.TestCase} tc */ export const testObserversUsingObservedeep = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Array>} */ const pathes = [] let calls = 0 map0.observeDeep(events => { events.forEach(event => { pathes.push(event.path) }) calls++ }) map0.set('map', new Y.Map()) map0.get('map').set('array', new Y.Array()) map0.get('map').get('array').insert(0, ['content']) t.assert(calls === 3) t.compare(pathes, [[], ['map'], ['map', 'array']]) compare(users) } /** * @param {t.TestCase} tc */ export const testPathsOfSiblingEvents = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Array>} */ const pathes = [] let calls = 0 const doc = users[0] map0.set('map', new Y.Map()) map0.get('map').set('text1', new Y.Text('initial')) map0.observeDeep(events => { events.forEach(event => { pathes.push(event.path) }) calls++ }) doc.transact(() => { map0.get('map').get('text1').insert(0, 'post-') map0.get('map').set('text2', new Y.Text('new')) }) t.assert(calls === 1) t.compare(pathes, [['map'], ['map', 'text1']]) compare(users) } // TODO: Test events in Y.Map /** * @param {Object} is * @param {Object} should */ const compareEvent = (is, should) => { for (const key in should) { t.compare(should[key], is[key]) } } /** * @param {t.TestCase} tc */ export const testThrowsAddAndUpdateAndDeleteEvents = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e // just put it on event, should be thrown synchronously anyway }) map0.set('stuff', 4) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in contents map0.set('stuff', new Y.Array()) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in opContents map0.set('stuff', 5) // delete map0.delete('stuff') compareEvent(event, { keysChanged: new Set(['stuff']), target: map0 }) compare(users) } /** * @param {t.TestCase} tc */ export const testThrowsDeleteEventsOnClear = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e // just put it on event, should be thrown synchronously anyway }) // set values map0.set('stuff', 4) map0.set('otherstuff', new Y.Array()) // clear map0.clear() compareEvent(event, { keysChanged: new Set(['stuff', 'otherstuff']), target: map0 }) compare(users) } /** * @param {t.TestCase} tc */ export const testChangeEvent = tc => { const { map0, users } = init(tc, { users: 2 }) /** * @type {any} */ let changes = null /** * @type {any} */ let keyChange = null map0.observe(e => { changes = e.changes }) map0.set('a', 1) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) map0.set('a', 2) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1) users[0].transact(() => { map0.set('a', 3) map0.set('a', 4) }) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2) users[0].transact(() => { map0.set('b', 1) map0.set('b', 2) }) keyChange = changes.keys.get('b') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) users[0].transact(() => { map0.set('c', 1) map0.delete('c') }) t.assert(changes !== null && changes.keys.size === 0) users[0].transact(() => { map0.set('d', 1) map0.set('d', 2) }) keyChange = changes.keys.get('d') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) compare(users) } /** * @param {t.TestCase} _tc */ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => { const doc = new Y.Doc() const map = doc.getMap('map') let updateCalled = false let throwingObserverCalled = false let throwingDeepObserverCalled = false doc.on('update', () => { updateCalled = true }) const throwingObserver = () => { throwingObserverCalled = true throw new Error('Failure') } const throwingDeepObserver = () => { throwingDeepObserverCalled = true throw new Error('Failure') } map.observe(throwingObserver) map.observeDeep(throwingDeepObserver) t.fails(() => { map.set('y', '2') }) t.assert(updateCalled) t.assert(throwingObserverCalled) t.assert(throwingDeepObserverCalled) // check if it works again updateCalled = false throwingObserverCalled = false throwingDeepObserverCalled = false t.fails(() => { map.set('z', '3') }) t.assert(updateCalled) t.assert(throwingObserverCalled) t.assert(throwingDeepObserverCalled) t.assert(map.get('z') === '3') } /** * @param {t.TestCase} tc */ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => { const { users, map0 } = init(tc, { users: 3 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e }) map0.set('stuff', 2) t.compare(event.value, event.target.get(event.name)) compare(users) } /** * @param {t.TestCase} tc */ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc => { const { users, map0, map1, testConnector } = init(tc, { users: 3 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e }) map1.set('stuff', 2) testConnector.flushAllMessages() t.compare(event.value, event.target.get(event.name)) compare(users) } /** * @type {Array} */ const mapTransactions = [ function set (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) const value = prng.utf16String(gen) user.getMap('map').set(key, value) }, function setType (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()]) user.getMap('map').set(key, type) if (type instanceof Y.Array) { type.insert(0, [1, 2, 3, 4]) } else { type.set('deepkey', 'deepvalue') } }, function _delete (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) user.getMap('map').delete(key) } ] /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests10 = tc => { applyRandomTests(tc, mapTransactions, 3) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests40 = tc => { applyRandomTests(tc, mapTransactions, 40) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests42 = tc => { applyRandomTests(tc, mapTransactions, 42) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests43 = tc => { applyRandomTests(tc, mapTransactions, 43) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests44 = tc => { applyRandomTests(tc, mapTransactions, 44) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests45 = tc => { applyRandomTests(tc, mapTransactions, 45) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests46 = tc => { applyRandomTests(tc, mapTransactions, 46) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests300 = tc => { applyRandomTests(tc, mapTransactions, 300) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests400 = tc => { applyRandomTests(tc, mapTransactions, 400) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests500 = tc => { applyRandomTests(tc, mapTransactions, 500) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests600 = tc => { applyRandomTests(tc, mapTransactions, 600) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests1000 = tc => { applyRandomTests(tc, mapTransactions, 1000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests1800 = tc => { applyRandomTests(tc, mapTransactions, 1800) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests5000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 5000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests10000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 10000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests100000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 100000) } yjs-13.6.8/tests/y-text.tests.js000066400000000000000000001766261450200430400165230ustar00rootroot00000000000000import * as Y from './testHelper.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' import * as math from 'lib0/math' const { init, compare } = Y /** * https://github.com/yjs/yjs/issues/474 * @todo Remove debug: 127.0.0.1:8080/test.html?filter=\[88/ * @param {t.TestCase} _tc */ export const testDeltaBug = _tc => { const initialDelta = [{ attributes: { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' }, insert: '\n' }, { attributes: { 'table-col': { width: '150' } }, insert: '\n\n\n' }, { attributes: { 'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-apba4k' }, row: 'row-6kv2ls', cell: 'cell-apba4k', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-a8qf0r' }, row: 'row-6kv2ls', cell: 'cell-a8qf0r', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-oi9ikb' }, row: 'row-6kv2ls', cell: 'cell-oi9ikb', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-dt6ks2' }, row: 'row-d1sv2g', cell: 'cell-dt6ks2', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-qah2ay' }, row: 'row-d1sv2g', cell: 'cell-qah2ay', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-468a69b5-9332-450b-9107-381d593de249', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-fpcz5a' }, row: 'row-d1sv2g', cell: 'cell-fpcz5a', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-zrhylp' }, row: 'row-pflz90', cell: 'cell-zrhylp', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-s1q9nt' }, row: 'row-pflz90', cell: 'cell-s1q9nt', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-20b0j9' }, row: 'row-pflz90', cell: 'cell-20b0j9', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3' }, insert: '\n' }, { insert: 'Content after table' }, { attributes: { 'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce' }, insert: '\n' } ] const ydoc1 = new Y.Doc() const ytext = ydoc1.getText() ytext.applyDelta(initialDelta) const addingDash = [ { retain: 12 }, { insert: '-' } ] ytext.applyDelta(addingDash) const addingSpace = [ { retain: 13 }, { insert: ' ' } ] ytext.applyDelta(addingSpace) const addingList = [ { retain: 12 }, { delete: 2 }, { retain: 1, attributes: { // Clear table line attribute 'table-cell-line': null, // Add list attribute in place of table-cell-line list: { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-20b0j9', list: 'bullet' } } } ] ytext.applyDelta(addingList) const result = ytext.toDelta() const expectedResult = [ { attributes: { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' }, insert: '\n' }, { attributes: { 'table-col': { width: '150' } }, insert: '\n\n\n' }, { attributes: { 'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-apba4k' }, row: 'row-6kv2ls', cell: 'cell-apba4k', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-a8qf0r' }, row: 'row-6kv2ls', cell: 'cell-a8qf0r', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-6kv2ls', cell: 'cell-oi9ikb' }, row: 'row-6kv2ls', cell: 'cell-oi9ikb', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-dt6ks2' }, row: 'row-d1sv2g', cell: 'cell-dt6ks2', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-qah2ay' }, row: 'row-d1sv2g', cell: 'cell-qah2ay', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-468a69b5-9332-450b-9107-381d593de249', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-d1sv2g', cell: 'cell-fpcz5a' }, row: 'row-d1sv2g', cell: 'cell-fpcz5a', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-zrhylp' }, row: 'row-pflz90', cell: 'cell-zrhylp', rowspan: '1', colspan: '1' }, insert: '\n' }, { attributes: { 'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-s1q9nt' }, row: 'row-pflz90', cell: 'cell-s1q9nt', rowspan: '1', colspan: '1' }, insert: '\n' }, { insert: '\n', // This attibutes has only list and no table-cell-line attributes: { list: { rowspan: '1', colspan: '1', row: 'row-pflz90', cell: 'cell-20b0j9', list: 'bullet' }, 'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484', row: 'row-pflz90', cell: 'cell-20b0j9', rowspan: '1', colspan: '1' } }, // No table-cell-line below here { attributes: { 'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3' }, insert: '\n' }, { insert: 'Content after table' }, { attributes: { 'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce' }, insert: '\n' } ] t.compare(result, expectedResult) } /** * https://github.com/yjs/yjs/issues/503 * @param {t.TestCase} _tc */ export const testDeltaBug2 = _tc => { const initialContent = [ { insert: "Thomas' section" }, { insert: '\n', attributes: { 'block-id': 'block-61ae80ac-a469-4eae-bac9-3b6a2c380118' } }, { insert: '\n', attributes: { 'block-id': 'block-d265d93f-1cc7-40ee-bb58-8270fca2619f' } }, { insert: '123' }, { insert: '\n', attributes: { 'block-id': 'block-592a7bee-76a3-4e28-9c25-7a84344f8813', list: { list: 'toggled', 'toggle-id': 'list-66xfft' } } }, { insert: '456' }, { insert: '\n', attributes: { indent: 1, 'block-id': 'block-3ee2bd70-b97f-45b2-9115-f1e8910235b1', list: { list: 'toggled', 'toggle-id': 'list-6vh0t0' } } }, { insert: '789' }, { insert: '\n', attributes: { indent: 1, 'block-id': 'block-78150cf3-9bb5-4dea-a6f5-0ce1d2a98b9c', list: { list: 'toggled', 'toggle-id': 'list-7jr0l2' } } }, { insert: '901' }, { insert: '\n', attributes: { indent: 1, 'block-id': 'block-13c6416f-f522-41d5-9fd4-ce4eb1cde5ba', list: { list: 'toggled', 'toggle-id': 'list-7uk8qu' } } }, { insert: { slash_command: { id: 'doc_94zq-2436', sessionId: 'nkwc70p2j', replace: '/' } } }, { insert: '\n', attributes: { 'block-id': 'block-8a1d2bb6-23c2-4bcf-af3c-3919ffea1697' } }, { insert: '\n\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'block-id': 'block-84ec3ea4-da6a-4e03-b430-0e5f432936a9', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-blmd4s', cell: 'cell-m0u5za' }, row: 'row-blmd4s', cell: 'cell-m0u5za', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-83144ca8-aace-401e-8aa5-c05928a8ccf0', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-blmd4s', cell: 'cell-1v8s8t' }, row: 'row-blmd4s', cell: 'cell-1v8s8t', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-9a493387-d27f-4b58-b2f7-731dfafda32a', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-blmd4s', cell: 'cell-126947' }, row: 'row-blmd4s', cell: 'cell-126947', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-3484f86e-ae42-440f-8de6-857f0d8011ea', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-hmmljo', cell: 'cell-wvutl9' }, row: 'row-hmmljo', cell: 'cell-wvutl9', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-d4e0b741-9dea-47a5-85e1-4ded0efbc89d', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-hmmljo', cell: 'cell-nkablr' }, row: 'row-hmmljo', cell: 'cell-nkablr', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-352f0d5a-d1b9-422f-b136-4bacefd00b1a', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-hmmljo', cell: 'cell-n8xtd0' }, row: 'row-hmmljo', cell: 'cell-n8xtd0', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-95823e57-f29c-44cf-a69d-2b4494b7144b', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ev4xwq', cell: 'cell-ua9bvu' }, row: 'row-ev4xwq', cell: 'cell-ua9bvu', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-cde5c027-15d3-4780-9e76-1e1a9d97a8e8', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ev4xwq', cell: 'cell-7bwuvk' }, row: 'row-ev4xwq', cell: 'cell-7bwuvk', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-11a23ed4-b04d-4e45-8065-8120889cd4a4', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ev4xwq', cell: 'cell-aouka5' }, row: 'row-ev4xwq', cell: 'cell-aouka5', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-15b4483c-da98-4ded-91d3-c3d6ebc82582' } }, { insert: { divider: true } }, { insert: '\n', attributes: { 'block-id': 'block-68552c8e-b57b-4f4a-9f36-6cc1ef6b3461' } }, { insert: 'jklasjdf' }, { insert: '\n', attributes: { 'block-id': 'block-c8b2df7d-8ec5-4dd4-81f1-8d8efc40b1b4', list: { list: 'toggled', 'toggle-id': 'list-9ss39s' } } }, { insert: 'asdf' }, { insert: '\n', attributes: { 'block-id': 'block-4f252ceb-14da-49ae-8cbd-69a701d18e2a', list: { list: 'toggled', 'toggle-id': 'list-uvo013' } } }, { insert: 'adg' }, { insert: '\n', attributes: { 'block-id': 'block-ccb9b72e-b94d-45a0-aae4-9b0a1961c533', list: { list: 'toggled', 'toggle-id': 'list-k53iwe' } } }, { insert: 'asdfasdfasdf' }, { insert: '\n', attributes: { 'block-id': 'block-ccb9b72e-b94d-45a0-aae4-9b0a1961c533', list: { list: 'none' }, indent: 1 } }, { insert: 'asdf' }, { insert: '\n', attributes: { 'block-id': 'block-f406f76d-f338-4261-abe7-5c9131f7f1ad', list: { list: 'toggled', 'toggle-id': 'list-en86ur' } } }, { insert: '\n', attributes: { 'block-id': 'block-be18141c-9b6b-434e-8fd0-2c214437d560' } }, { insert: '\n', attributes: { 'block-id': 'block-36922db3-4af5-48a1-9ea4-0788b3b5d7cf' } }, { insert: { table_content: true } }, { insert: ' ' }, { insert: { slash_command: { id: 'doc_94zq-2436', sessionId: 'hiyrt6fny', replace: '/' } } }, { insert: '\n', attributes: { 'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143' } }, { insert: '\n', attributes: { 'block-id': 'block-4b545085-114d-4d07-844c-789710ec3aab', layout: '12d887e1-d1a2-4814-a1a3-0c904e950b46_1185cd29-ef1b-45d5-8fda-51a70b704e64', 'layout-width': '0.25' } }, { insert: '\n', attributes: { 'block-id': 'block-4d3f2321-33d1-470e-9b7c-d5a683570148', layout: '12d887e1-d1a2-4814-a1a3-0c904e950b46_75523ea3-c67f-4f5f-a85f-ac7c8fc0a992', 'layout-width': '0.5' } }, { insert: '\n', attributes: { 'block-id': 'block-4c7ae1e6-758e-470f-8d7c-ae0325e4ee8a', layout: '12d887e1-d1a2-4814-a1a3-0c904e950b46_54c740ef-fd7b-48c6-85aa-c14e1bfc9297', 'layout-width': '0.25' } }, { insert: '\n', attributes: { 'block-id': 'block-2d6ff0f4-ff00-42b7-a8e2-b816463d8fb5' } }, { insert: { divider: true } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'table-col': { width: '154' } } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'block-id': 'block-38545d56-224b-464c-b779-51fcec24dbbf', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-q0qfck', cell: 'cell-hmapv4' }, row: 'row-q0qfck', cell: 'cell-hmapv4', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-d413a094-5f52-4fd4-a4aa-00774f6fdb44', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-q0qfck', cell: 'cell-c0czb2' }, row: 'row-q0qfck', cell: 'cell-c0czb2', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-ff855cbc-8871-4e0a-9ba7-de0c1c2aa585', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-q0qfck', cell: 'cell-hcpqmm' }, row: 'row-q0qfck', cell: 'cell-hcpqmm', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-4841e6ee-fef8-4473-bf04-f5ba62db17f0', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-etopyl', cell: 'cell-0io73v' }, row: 'row-etopyl', cell: 'cell-0io73v', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-adeec631-d4fe-4f38-9d5e-e67ba068bd24', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-etopyl', cell: 'cell-gt2waa' }, row: 'row-etopyl', cell: 'cell-gt2waa', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-d38a7308-c858-4ce0-b1f3-0f9092384961', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-etopyl', cell: 'cell-os9ksy' }, row: 'row-etopyl', cell: 'cell-os9ksy', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-a9df6568-1838-40d1-9d16-3c073b6ce169', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-hbx9ri' }, row: 'row-0jwjg3', cell: 'cell-hbx9ri', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-e26a0cf2-fe62-44a5-a4ca-8678a56d62f1', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-yg5m2w' }, row: 'row-0jwjg3', cell: 'cell-yg5m2w', rowspan: '1', colspan: '1' } }, { insert: 'a' }, { insert: '\n', attributes: { 'block-id': 'block-bfbc5ac2-7417-44b9-9aa5-8e36e4095627', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'b' }, { insert: '\n', attributes: { 'block-id': 'block-f011c089-6389-47c0-8396-7477a29aa56f', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'c' }, { insert: '\n', attributes: { 'block-id': 'block-4497788d-1e02-4fd5-a80a-48b61a6185cb', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'd' }, { insert: '\n', attributes: { 'block-id': 'block-5d73a2c7-f98b-47c7-a3f5-0d8527962b02', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'e' }, { insert: '\n', attributes: { 'block-id': 'block-bfda76ee-ffdd-45db-a22e-a6707e11cf68', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'd' }, { insert: '\n', attributes: { 'block-id': 'block-35242e64-a69d-4cdb-bd85-2a93766bfab4', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: 'f' }, { insert: '\n', attributes: { 'block-id': 'block-8baa22c8-491b-4f1b-9502-44179d5ae744', list: { rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2', list: 'ordered' }, rowspan: '1', colspan: '1', row: 'row-0jwjg3', cell: 'cell-1azhl2' } }, { insert: '\n', attributes: { 'block-id': 'block-7fa64af0-6974-4205-8cee-529f8bd46852' } }, { insert: { divider: true } }, { insert: "Brandon's Section" }, { insert: '\n', attributes: { header: 2, 'block-id': 'block-cf49462c-2370-48ff-969d-576cb32c39a1' } }, { insert: '\n', attributes: { 'block-id': 'block-30ef8361-0dd6-4eee-b4eb-c9012d0e9070' } }, { insert: { slash_command: { id: 'doc_94zq-2436', sessionId: 'x9x08o916', replace: '/' } } }, { insert: '\n', attributes: { 'block-id': 'block-166ed856-cf8c-486a-9365-f499b21d91b3' } }, { insert: { divider: true } }, { insert: '\n', attributes: { row: 'row-kssn15', rowspan: '1', colspan: '1', 'block-id': 'block-e8079594-4559-4259-98bb-da5280e2a692', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-kssn15', cell: 'cell-qxbksf' }, cell: 'cell-qxbksf' } }, { insert: '\n', attributes: { 'block-id': 'block-70132663-14cc-4701-b5c5-eb99e875e2bd', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-kssn15', cell: 'cell-lsohbx' }, cell: 'cell-lsohbx', row: 'row-kssn15', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-47a3899c-e3c5-4a7a-a8c4-46e0ae73a4fa', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-kssn15', cell: 'cell-hner9k' }, cell: 'cell-hner9k', row: 'row-kssn15', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-0f9e650a-7841-412e-b4f2-5571b6d352c2', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-juxwc0', cell: 'cell-ei4yqp' }, cell: 'cell-ei4yqp', row: 'row-juxwc0', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-53a158a9-8c82-4c82-9d4e-f5298257ca43', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-juxwc0', cell: 'cell-25pf5x' }, cell: 'cell-25pf5x', row: 'row-juxwc0', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-da8ba35e-ce6e-4518-8605-c51d781eb07a', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-juxwc0', cell: 'cell-m8reor' }, cell: 'cell-m8reor', row: 'row-juxwc0', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-2dce37c7-2978-4127-bed0-9549781babcb', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ot4wy5', cell: 'cell-dinh0i' }, cell: 'cell-dinh0i', row: 'row-ot4wy5', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-7b593f8c-4ea3-44b4-8ad9-4a0abffe759b', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ot4wy5', cell: 'cell-d115b2' }, cell: 'cell-d115b2', row: 'row-ot4wy5', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-272c28e6-2bde-4477-9d99-ce35b3045895', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-ot4wy5', cell: 'cell-fuapvo' }, cell: 'cell-fuapvo', row: 'row-ot4wy5', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-fbf23cab-1ce9-4ede-9953-f2f8250004cf' } }, { insert: '\n', attributes: { 'block-id': 'block-c3fbb8c9-495c-40b0-b0dd-f6e33dd64b1b' } }, { insert: '\n', attributes: { 'block-id': 'block-3417ad09-92a3-4a43-b5db-6dbcb0f16db4' } }, { insert: '\n', attributes: { 'block-id': 'block-b9eacdce-4ba3-4e66-8b69-3eace5656057' } }, { insert: 'Dan Gornstein' }, { insert: '\n', attributes: { 'block-id': 'block-d7c6ae0d-a17c-433e-85fd-5efc52b587fb', header: 1 } }, { insert: '\n', attributes: { 'block-id': 'block-814521bd-0e14-4fbf-b332-799c6452a624' } }, { insert: 'aaa' }, { insert: '\n', attributes: { 'block-id': 'block-6aaf4dcf-dc21-45c6-b723-afb25fe0f498', list: { list: 'toggled', 'toggle-id': 'list-idl93b' } } }, { insert: 'bb' }, { insert: '\n', attributes: { indent: 1, 'block-id': 'block-3dd75392-fa50-4bfb-ba6b-3b7d6bd3f1a1', list: { list: 'toggled', 'toggle-id': 'list-mrq7j2' } } }, { insert: 'ccc' }, { insert: '\n', attributes: { 'block-id': 'block-2528578b-ecda-4f74-9fd7-8741d72dc8b3', indent: 2, list: { list: 'toggled', 'toggle-id': 'list-liu7dl' } } }, { insert: '\n', attributes: { 'block-id': 'block-18bf68c3-9ef3-4874-929c-9b6bb1a00325' } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n', attributes: { 'block-id': 'block-d44e74b4-b37f-48e0-b319-6327a6295a57', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie' }, row: 'row-si1nah', cell: 'cell-cpybie', rowspan: '1', colspan: '1' } }, { insert: 'aaa' }, { insert: '\n', attributes: { 'block-id': 'block-3e545ee9-0c9a-42d7-a4d0-833edb8087f3', list: { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie', list: 'toggled', 'toggle-id': 'list-kjl2ik' }, rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie' } }, { insert: 'bb' }, { insert: '\n', attributes: { indent: 1, 'block-id': 'block-5f1225ad-370f-46ab-8f1e-18b277b5095f', list: { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie', list: 'toggled', 'toggle-id': 'list-eei1x5' }, rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie' } }, { insert: 'ccc' }, { insert: '\n', attributes: { indent: 2, 'block-id': 'block-a77fdc11-ad24-431b-9ca2-09e32db94ac2', list: { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie', list: 'toggled', 'toggle-id': 'list-30us3c' }, rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie' } }, { insert: '\n', attributes: { 'block-id': 'block-d44e74b4-b37f-48e0-b319-6327a6295a57', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-cpybie' }, row: 'row-si1nah', cell: 'cell-cpybie', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-2c274c8a-757d-4892-8db8-1a7999f7ab51', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-al1z64' }, row: 'row-si1nah', cell: 'cell-al1z64', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-85931afe-1879-471c-bb4b-89e7bd517fe9', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-si1nah', cell: 'cell-q186pb' }, row: 'row-si1nah', cell: 'cell-q186pb', rowspan: '1', colspan: '1' } }, { insert: 'asdfasdfasdf' }, { insert: '\n', attributes: { 'block-id': 'block-6e0522e8-c1eb-4c07-98df-2b07c533a139', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-7x2d1o', cell: 'cell-6eid2t' }, row: 'row-7x2d1o', cell: 'cell-6eid2t', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-4b3d0bd0-9175-45e9-955c-e8164f4b5376', row: 'row-7x2d1o', cell: 'cell-m1alad', rowspan: '1', colspan: '1', list: { rowspan: '1', colspan: '1', row: 'row-7x2d1o', cell: 'cell-m1alad', list: 'ordered' } } }, { insert: 'asdfasdfasdf' }, { insert: '\n', attributes: { 'block-id': 'block-08610089-cb05-4366-bb1e-a0787d5b11bf', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-7x2d1o', cell: 'cell-dm1l2p' }, row: 'row-7x2d1o', cell: 'cell-dm1l2p', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-c22b5125-8df3-432f-bd55-5ff456e41b4e', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-o0ujua', cell: 'cell-82g0ca' }, row: 'row-o0ujua', cell: 'cell-82g0ca', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-7c6320e4-acaf-4ab4-8355-c9b00408c9c1', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-o0ujua', cell: 'cell-wv6ozp' }, row: 'row-o0ujua', cell: 'cell-wv6ozp', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-d1bb7bed-e69e-4807-8d20-2d28fef8d08f', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-o0ujua', cell: 'cell-ldt53x' }, row: 'row-o0ujua', cell: 'cell-ldt53x', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-28f28cb8-51a2-4156-acf9-2380e1349745' } }, { insert: { divider: true } }, { insert: '\n', attributes: { 'block-id': 'block-a1193252-c0c8-47fe-b9f6-32c8b01a1619' } }, { insert: '\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n\n', attributes: { 'table-col': { width: '150' } } }, { insert: '/This is a test.' }, { insert: '\n', attributes: { 'block-id': 'block-14188df0-a63f-4317-9a6d-91b96a7ac9fe', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5ixdvv', cell: 'cell-9tgyed' }, row: 'row-5ixdvv', cell: 'cell-9tgyed', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-7e5ba2af-9903-457d-adf4-2a79be81d823', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5ixdvv', cell: 'cell-xc56e9' }, row: 'row-5ixdvv', cell: 'cell-xc56e9', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-eb6cad93-caf7-4848-8adf-415255139268', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5ixdvv', cell: 'cell-xrze3u' }, row: 'row-5ixdvv', cell: 'cell-xrze3u', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-5bb547a2-6f71-4624-80c7-d0e1318c81a2', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-xbzv98', cell: 'cell-lie0ng' }, row: 'row-xbzv98', cell: 'cell-lie0ng', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-b506de0d-efb6-4bd7-ba8e-2186cc57903e', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-xbzv98', cell: 'cell-s9sow1' }, row: 'row-xbzv98', cell: 'cell-s9sow1', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-42d2ad20-5521-40e3-a88d-fe6906176e61', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-xbzv98', cell: 'cell-nodtcj' }, row: 'row-xbzv98', cell: 'cell-nodtcj', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-7d3e4216-3f68-4dd6-bc77-4a9fad4ba008', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5bqfil', cell: 'cell-c8c0f3' }, row: 'row-5bqfil', cell: 'cell-c8c0f3', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-6671f221-551e-47fb-9b7d-9043b6b12cdc', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5bqfil', cell: 'cell-jvxxif' }, row: 'row-5bqfil', cell: 'cell-jvxxif', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-51e3161b-0437-4fe3-ac4f-129a93a93fc3', 'table-cell-line': { rowspan: '1', colspan: '1', row: 'row-5bqfil', cell: 'cell-rmjpze' }, row: 'row-5bqfil', cell: 'cell-rmjpze', rowspan: '1', colspan: '1' } }, { insert: '\n', attributes: { 'block-id': 'block-21099df0-afb2-4cd3-834d-bb37800eb06a' } } ] const ydoc = new Y.Doc() const ytext = ydoc.getText('id') ytext.applyDelta(initialContent) const changeEvent = [ { retain: 90 }, { delete: 4 }, { retain: 1, attributes: { layout: null, 'layout-width': null, 'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143' } } ] ytext.applyDelta(changeEvent) const delta = ytext.toDelta() t.compare(delta[41], { insert: '\n', attributes: { 'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143' } }) } /** * In this test we are mainly interested in the cleanup behavior and whether the resulting delta makes sense. * It is fine if the resulting delta is not minimal. But applying the delta to a rich-text editor should result in a * synced document. * * @param {t.TestCase} tc */ export const testDeltaAfterConcurrentFormatting = tc => { const { text0, text1, testConnector } = init(tc, { users: 2 }) text0.insert(0, 'abcde') testConnector.flushAllMessages() text0.format(0, 3, { bold: true }) text1.format(2, 2, { bold: true }) /** * @type {any} */ const deltas = [] text1.observe(event => { if (event.delta.length > 0) { deltas.push(event.delta) } }) testConnector.flushAllMessages() t.compare(deltas, [[{ retain: 3, attributes: { bold: true } }, { retain: 2, attributes: { bold: null } }]]) } /** * @param {t.TestCase} tc */ export const testBasicInsertAndDelete = tc => { const { users, text0 } = init(tc, { users: 2 }) let delta text0.observe(event => { delta = event.delta }) text0.delete(0, 0) t.assert(true, 'Does not throw when deleting zero elements with position 0') text0.insert(0, 'abc') t.assert(text0.toString() === 'abc', 'Basic insert works') t.compare(delta, [{ insert: 'abc' }]) text0.delete(0, 1) t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)') t.compare(delta, [{ delete: 1 }]) text0.delete(1, 1) t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.compare(delta, [{ retain: 1 }, { delete: 1 }]) users[0].transact(() => { text0.insert(0, '1') text0.delete(0, 1) }) t.compare(delta, []) compare(users) } /** * @param {t.TestCase} tc */ export const testBasicFormat = tc => { const { users, text0 } = init(tc, { users: 2 }) let delta text0.observe(event => { delta = event.delta }) text0.insert(0, 'abc', { bold: true }) t.assert(text0.toString() === 'abc', 'Basic insert with attributes works') t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }]) t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }]) text0.delete(0, 1) t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)') t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }]) t.compare(delta, [{ delete: 1 }]) text0.delete(1, 1) t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }]) t.compare(delta, [{ retain: 1 }, { delete: 1 }]) text0.insert(0, 'z', { bold: true }) t.assert(text0.toString() === 'zb') t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }]) t.compare(delta, [{ insert: 'z', attributes: { bold: true } }]) // @ts-ignore t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker') text0.insert(0, 'y') t.assert(text0.toString() === 'yzb') t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }]) t.compare(delta, [{ insert: 'y' }]) text0.format(0, 2, { bold: null }) t.assert(text0.toString() === 'yzb') t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }]) t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }]) compare(users) } /** * @param {t.TestCase} _tc */ export const testMultilineFormat = _tc => { const ydoc = new Y.Doc() const testText = ydoc.getText('test') testText.insert(0, 'Test\nMulti-line\nFormatting') testText.applyDelta([ { retain: 4, attributes: { bold: true } }, { retain: 1 }, // newline character { retain: 10, attributes: { bold: true } }, { retain: 1 }, // newline character { retain: 10, attributes: { bold: true } } ]) t.compare(testText.toDelta(), [ { insert: 'Test', attributes: { bold: true } }, { insert: '\n' }, { insert: 'Multi-line', attributes: { bold: true } }, { insert: '\n' }, { insert: 'Formatting', attributes: { bold: true } } ]) } /** * @param {t.TestCase} _tc */ export const testNotMergeEmptyLinesFormat = _tc => { const ydoc = new Y.Doc() const testText = ydoc.getText('test') testText.applyDelta([ { insert: 'Text' }, { insert: '\n', attributes: { title: true } }, { insert: '\nText' }, { insert: '\n', attributes: { title: true } } ]) t.compare(testText.toDelta(), [ { insert: 'Text' }, { insert: '\n', attributes: { title: true } }, { insert: '\nText' }, { insert: '\n', attributes: { title: true } } ]) } /** * @param {t.TestCase} _tc */ export const testPreserveAttributesThroughDelete = _tc => { const ydoc = new Y.Doc() const testText = ydoc.getText('test') testText.applyDelta([ { insert: 'Text' }, { insert: '\n', attributes: { title: true } }, { insert: '\n' } ]) testText.applyDelta([ { retain: 4 }, { delete: 1 }, { retain: 1, attributes: { title: true } } ]) t.compare(testText.toDelta(), [ { insert: 'Text' }, { insert: '\n', attributes: { title: true } } ]) } /** * @param {t.TestCase} tc */ export const testGetDeltaWithEmbeds = tc => { const { text0 } = init(tc, { users: 1 }) text0.applyDelta([{ insert: { linebreak: 's' } }]) t.compare(text0.toDelta(), [{ insert: { linebreak: 's' } }]) } /** * @param {t.TestCase} tc */ export const testTypesAsEmbed = tc => { const { text0, text1, testConnector } = init(tc, { users: 2 }) text0.applyDelta([{ insert: new Y.Map([['key', 'val']]) }]) t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' }) let firedEvent = false text1.observe(event => { const d = event.delta t.assert(d.length === 1) t.compare(d.map(x => /** @type {Y.AbstractType} */ (x.insert).toJSON()), [{ key: 'val' }]) firedEvent = true }) testConnector.flushAllMessages() const delta = text1.toDelta() t.assert(delta.length === 1) t.compare(delta[0].insert.toJSON(), { key: 'val' }) t.assert(firedEvent, 'fired the event observer containing a Type-Embed') } /** * @param {t.TestCase} tc */ export const testSnapshot = tc => { const { text0 } = init(tc, { users: 1 }) const doc0 = /** @type {Y.Doc} */ (text0.doc) doc0.gc = false text0.applyDelta([{ insert: 'abcd' }]) const snapshot1 = Y.snapshot(doc0) text0.applyDelta([{ retain: 1 }, { insert: 'x' }, { delete: 1 }]) const snapshot2 = Y.snapshot(doc0) text0.applyDelta([{ retain: 2 }, { delete: 3 }, { insert: 'x' }, { delete: 1 }]) const state1 = text0.toDelta(snapshot1) t.compare(state1, [{ insert: 'abcd' }]) const state2 = text0.toDelta(snapshot2) t.compare(state2, [{ insert: 'axcd' }]) const state2Diff = text0.toDelta(snapshot2, snapshot1) // @ts-ignore Remove userid info state2Diff.forEach(v => { if (v.attributes && v.attributes.ychange) { delete v.attributes.ychange.user } }) t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }]) } /** * @param {t.TestCase} tc */ export const testSnapshotDeleteAfter = tc => { const { text0 } = init(tc, { users: 1 }) const doc0 = /** @type {Y.Doc} */ (text0.doc) doc0.gc = false text0.applyDelta([{ insert: 'abcd' }]) const snapshot1 = Y.snapshot(doc0) text0.applyDelta([{ retain: 4 }, { insert: 'e' }]) const state1 = text0.toDelta(snapshot1) t.compare(state1, [{ insert: 'abcd' }]) } /** * @param {t.TestCase} tc */ export const testToJson = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'abc', { bold: true }) t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text') } /** * @param {t.TestCase} tc */ export const testToDeltaEmbedAttributes = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'ab', { bold: true }) text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 }) const delta0 = text0.toDelta() t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }]) } /** * @param {t.TestCase} tc */ export const testToDeltaEmbedNoAttributes = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'ab', { bold: true }) text0.insertEmbed(1, { image: 'imageSrc.png' }) const delta0 = text0.toDelta() t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present') } /** * @param {t.TestCase} tc */ export const testFormattingRemoved = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'ab', { bold: true }) text0.delete(0, 2) t.assert(Y.getTypeChildren(text0).length === 1) } /** * @param {t.TestCase} tc */ export const testFormattingRemovedInMidText = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, '1234') text0.insert(2, 'ab', { bold: true }) text0.delete(2, 2) t.assert(Y.getTypeChildren(text0).length === 3) } /** * Reported in https://github.com/yjs/yjs/issues/344 * * @param {t.TestCase} tc */ export const testFormattingDeltaUnnecessaryAttributeChange = tc => { const { text0, text1, testConnector } = init(tc, { users: 2 }) text0.insert(0, '\n', { PARAGRAPH_STYLES: 'normal', LIST_STYLES: 'bullet' }) text0.insert(1, 'abc', { PARAGRAPH_STYLES: 'normal' }) testConnector.flushAllMessages() /** * @type {Array} */ const deltas = [] text0.observe(event => { deltas.push(event.delta) }) text1.observe(event => { deltas.push(event.delta) }) text1.format(0, 1, { LIST_STYLES: 'number' }) testConnector.flushAllMessages() const filteredDeltas = deltas.filter(d => d.length > 0) t.assert(filteredDeltas.length === 2) t.compare(filteredDeltas[0], [ { retain: 1, attributes: { LIST_STYLES: 'number' } } ]) t.compare(filteredDeltas[0], filteredDeltas[1]) } /** * @param {t.TestCase} tc */ export const testInsertAndDeleteAtRandomPositions = tc => { const N = 100000 const { text0 } = init(tc, { users: 1 }) const gen = tc.prng // create initial content // let expectedResult = init text0.insert(0, prng.word(gen, N / 2, N / 2)) // apply changes for (let i = 0; i < N; i++) { const pos = prng.uint32(gen, 0, text0.length) if (prng.bool(gen)) { const len = prng.uint32(gen, 1, 5) const word = prng.word(gen, 0, len) text0.insert(pos, word) // expectedResult = expectedResult.slice(0, pos) + word + expectedResult.slice(pos) } else { const len = prng.uint32(gen, 0, math.min(3, text0.length - pos)) text0.delete(pos, len) // expectedResult = expectedResult.slice(0, pos) + expectedResult.slice(pos + len) } } // t.compareStrings(text0.toString(), expectedResult) t.describe('final length', '' + text0.length) } /** * @param {t.TestCase} tc */ export const testAppendChars = tc => { const N = 10000 const { text0 } = init(tc, { users: 1 }) // apply changes for (let i = 0; i < N; i++) { text0.insert(text0.length, 'a') } t.assert(text0.length === N) } const largeDocumentSize = 100000 const id = Y.createID(0, 0) const c = new Y.ContentString('a') /** * @param {t.TestCase} _tc */ export const testBestCase = _tc => { const N = largeDocumentSize const items = new Array(N) t.measureTime('time to create two million items in the best case', () => { const parent = /** @type {any} */ ({}) let prevItem = null for (let i = 0; i < N; i++) { /** * @type {Y.Item} */ const n = new Y.Item(Y.createID(0, 0), null, null, null, null, null, null, c) // items.push(n) items[i] = n n.right = prevItem n.rightOrigin = prevItem ? id : null n.content = c n.parent = parent prevItem = n } }) const newArray = new Array(N) t.measureTime('time to copy two million items to new Array', () => { for (let i = 0; i < N; i++) { newArray[i] = items[i] } }) } const tryGc = () => { // @ts-ignore if (typeof global !== 'undefined' && global.gc) { // @ts-ignore global.gc() } } /** * @param {t.TestCase} _tc */ export const testLargeFragmentedDocument = _tc => { const itemsToInsert = largeDocumentSize let update = /** @type {any} */ (null) ;(() => { const doc1 = new Y.Doc() const text0 = doc1.getText('txt') tryGc() t.measureTime(`time to insert ${itemsToInsert} items`, () => { doc1.transact(() => { for (let i = 0; i < itemsToInsert; i++) { text0.insert(0, '0') } }) }) tryGc() t.measureTime('time to encode document', () => { update = Y.encodeStateAsUpdateV2(doc1) }) t.describe('Document size:', update.byteLength) })() ;(() => { const doc2 = new Y.Doc() tryGc() t.measureTime(`time to apply ${itemsToInsert} updates`, () => { Y.applyUpdateV2(doc2, update) }) })() } /** * @param {t.TestCase} _tc */ export const testIncrementalUpdatesPerformanceOnLargeFragmentedDocument = _tc => { const itemsToInsert = largeDocumentSize const updates = /** @type {Array} */ ([]) ;(() => { const doc1 = new Y.Doc() doc1.on('update', update => { updates.push(update) }) const text0 = doc1.getText('txt') tryGc() t.measureTime(`time to insert ${itemsToInsert} items`, () => { doc1.transact(() => { for (let i = 0; i < itemsToInsert; i++) { text0.insert(0, '0') } }) }) tryGc() })() ;(() => { t.measureTime(`time to merge ${itemsToInsert} updates (differential updates)`, () => { Y.mergeUpdates(updates) }) tryGc() t.measureTime(`time to merge ${itemsToInsert} updates (ydoc updates)`, () => { const ydoc = new Y.Doc() updates.forEach(update => { Y.applyUpdate(ydoc, update) }) }) })() } /** * Splitting surrogates can lead to invalid encoded documents. * * https://github.com/yjs/yjs/issues/248 * * @param {t.TestCase} tc */ export const testSplitSurrogateCharacter = tc => { { const { users, text0 } = init(tc, { users: 2 }) users[1].disconnect() // disconnecting forces the user to encode the split surrogate text0.insert(0, 'πŸ‘Ύ') // insert surrogate character // split surrogate, which should not lead to an encoding error text0.insert(1, 'hi!') compare(users) } { const { users, text0 } = init(tc, { users: 2 }) users[1].disconnect() // disconnecting forces the user to encode the split surrogate text0.insert(0, 'πŸ‘ΎπŸ‘Ύ') // insert surrogate character // partially delete surrogate text0.delete(1, 2) compare(users) } { const { users, text0 } = init(tc, { users: 2 }) users[1].disconnect() // disconnecting forces the user to encode the split surrogate text0.insert(0, 'πŸ‘ΎπŸ‘Ύ') // insert surrogate character // formatting will also split surrogates text0.format(1, 2, { bold: true }) compare(users) } } /** * Search marker bug https://github.com/yjs/yjs/issues/307 * * @param {t.TestCase} tc */ export const testSearchMarkerBug1 = tc => { const { users, text0, text1, testConnector } = init(tc, { users: 2 }) users[0].on('update', update => { users[0].transact(() => { Y.applyUpdate(users[0], update) }) }) users[0].on('update', update => { users[1].transact(() => { Y.applyUpdate(users[1], update) }) }) text0.insert(0, 'a_a') testConnector.flushAllMessages() text0.insert(2, 's') testConnector.flushAllMessages() text1.insert(3, 'd') testConnector.flushAllMessages() text0.delete(0, 5) testConnector.flushAllMessages() text0.insert(0, 'a_a') testConnector.flushAllMessages() text0.insert(2, 's') testConnector.flushAllMessages() text1.insert(3, 'd') testConnector.flushAllMessages() t.compareStrings(text0.toString(), text1.toString()) t.compareStrings(text0.toString(), 'a_sda') compare(users) } /** * Reported in https://github.com/yjs/yjs/pull/32 * * @param {t.TestCase} _tc */ export const testFormattingBug = async _tc => { const ydoc1 = new Y.Doc() const ydoc2 = new Y.Doc() const text1 = ydoc1.getText() text1.insert(0, '\n\n\n') text1.format(0, 3, { url: 'http://example.com' }) ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' }) ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' }) Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1)) const text2 = ydoc2.getText() const expectedResult = [ { insert: '\n', attributes: { url: 'http://example.com' } }, { insert: '\n', attributes: { url: 'http://docs.yjs.dev' } }, { insert: '\n', attributes: { url: 'http://example.com' } } ] t.compare(text1.toDelta(), expectedResult) t.compare(text1.toDelta(), text2.toDelta()) console.log(text1.toDelta()) } /** * Delete formatting should not leave redundant formatting items. * * @param {t.TestCase} _tc */ export const testDeleteFormatting = _tc => { const doc = new Y.Doc() const text = doc.getText() text.insert(0, 'Attack ships on fire off the shoulder of Orion.') const doc2 = new Y.Doc() const text2 = doc2.getText() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) text.format(13, 7, { bold: true }) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) text.format(16, 4, { bold: null }) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) const expected = [ { insert: 'Attack ships ' }, { insert: 'on ', attributes: { bold: true } }, { insert: 'fire off the shoulder of Orion.' } ] t.compare(text.toDelta(), expected) t.compare(text2.toDelta(), expected) } // RANDOM TESTS let charCounter = 0 /** * Random tests for pure text operations without formatting. * * @type Array */ const textChanges = [ /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // insert text const ytext = y.getText('text') const insertPos = prng.int32(gen, 0, ytext.length) const text = charCounter++ + prng.word(gen) const prevText = ytext.toString() ytext.insert(insertPos, text) t.compareStrings(ytext.toString(), prevText.slice(0, insertPos) + text + prevText.slice(insertPos)) }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // delete text const ytext = y.getText('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) const prevText = ytext.toString() ytext.delete(insertPos, overwrite) t.compareStrings(ytext.toString(), prevText.slice(0, insertPos) + prevText.slice(insertPos + overwrite)) } ] /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges5 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 5)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges30 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 30)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges40 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 40)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges50 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 50)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges70 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 70)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges90 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 90)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateTextChanges300 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, textChanges, 300)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } const marks = [ { bold: true }, { italic: true }, { italic: true, color: '#888' } ] const marksChoices = [ undefined, ...marks ] /** * Random tests for all features of y-text (formatting, embeds, ..). * * @type Array */ const qChanges = [ /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // insert text const ytext = y.getText('text') const insertPos = prng.int32(gen, 0, ytext.length) const attrs = prng.oneOf(gen, marksChoices) const text = charCounter++ + prng.word(gen) ytext.insert(insertPos, text, attrs) }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // insert embed const ytext = y.getText('text') const insertPos = prng.int32(gen, 0, ytext.length) if (prng.bool(gen)) { ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }) } else { ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]])) } }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // delete text const ytext = y.getText('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) ytext.delete(insertPos, overwrite) }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // format text const ytext = y.getText('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) const format = prng.oneOf(gen, marks) ytext.format(insertPos, overwrite, format) }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // insert codeblock const ytext = y.getText('text') const insertPos = prng.int32(gen, 0, ytext.toString().length) const text = charCounter++ + prng.word(gen) const ops = [] if (insertPos > 0) { ops.push({ retain: insertPos }) } ops.push({ insert: text }, { insert: '\n', format: { 'code-block': true } }) ytext.applyDelta(ops) }, /** * @param {Y.Doc} y * @param {prng.PRNG} gen */ (y, gen) => { // complex delta op const ytext = y.getText('text') const contentLen = ytext.toString().length let currentPos = math.max(0, prng.int32(gen, 0, contentLen - 1)) /** * @type {Array} */ const ops = currentPos > 0 ? [{ retain: currentPos }] : [] // create max 3 ops for (let i = 0; i < 7 && currentPos < contentLen; i++) { prng.oneOf(gen, [ () => { // format const retain = math.min(prng.int32(gen, 0, contentLen - currentPos), 5) const format = prng.oneOf(gen, marks) ops.push({ retain, attributes: format }) currentPos += retain }, () => { // insert const attrs = prng.oneOf(gen, marksChoices) const text = prng.word(gen, 1, 3) ops.push({ insert: text, attributes: attrs }) }, () => { // delete const delLen = math.min(prng.int32(gen, 0, contentLen - currentPos), 10) ops.push({ delete: delLen }) currentPos += delLen } ])() } ytext.applyDelta(ops) } ] /** * @param {any} result */ const checkResult = result => { for (let i = 1; i < result.testObjects.length; i++) { /** * @param {any} d */ const typeToObject = d => d.insert instanceof Y.AbstractType ? d.insert.toJSON() : d const p1 = result.users[i].getText('text').toDelta().map(typeToObject) const p2 = result.users[i].getText('text').toDelta().map(typeToObject) t.compare(p1, p2) } // Uncomment this to find formatting-cleanup issues // const cleanups = Y.cleanupYTextFormatting(result.users[0].getText('text')) // t.assert(cleanups === 0) return result } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges1 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 1)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges2 = tc => { const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges2Repeat = tc => { for (let i = 0; i < 1000; i++) { const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2)) const cleanups = Y.cleanupYTextFormatting(users[0].getText('text')) t.assert(cleanups === 0) } } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges3 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 3)) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges30 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 30)) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges40 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 40)) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges70 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 70)) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges100 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 100)) } /** * @param {t.TestCase} tc */ export const testRepeatGenerateQuillChanges300 = tc => { checkResult(Y.applyRandomTests(tc, qChanges, 300)) } yjs-13.6.8/tests/y-xml.tests.js000066400000000000000000000146601450200430400163240ustar00rootroot00000000000000import { init, compare } from './testHelper.js' import * as Y from '../src/index.js' import * as t from 'lib0/testing' export const testCustomTypings = () => { const ydoc = new Y.Doc() const ymap = ydoc.getMap() /** * @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>} */ const yxml = ymap.set('yxml', new Y.XmlElement('test')) /** * @type {number|undefined} */ const num = yxml.getAttribute('num') /** * @type {string|undefined} */ const str = yxml.getAttribute('str') /** * @type {object|number|string|undefined} */ const dtrn = yxml.getAttribute('dtrn') const attrs = yxml.getAttributes() /** * @type {object|number|string|undefined} */ const any = attrs.shouldBeAny console.log({ num, str, dtrn, attrs, any }) } /** * @param {t.TestCase} tc */ export const testSetProperty = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) xml0.setAttribute('height', '10') t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works') testConnector.flushAllMessages() t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)') compare(users) } /** * @param {t.TestCase} tc */ export const testHasProperty = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) xml0.setAttribute('height', '10') t.assert(xml0.hasAttribute('height'), 'Simple set+has works') testConnector.flushAllMessages() t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)') xml0.removeAttribute('height') t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works') testConnector.flushAllMessages() t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)') compare(users) } /** * @param {t.TestCase} tc */ export const testEvents = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) /** * @type {any} */ let event /** * @type {any} */ let remoteEvent xml0.observe(e => { event = e }) xml1.observe(e => { remoteEvent = e }) xml0.setAttribute('key', 'value') t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key') testConnector.flushAllMessages() t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)') // check attributeRemoved xml0.removeAttribute('key') t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute') testConnector.flushAllMessages() t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)') xml0.insert(0, [new Y.XmlText('some text')]) t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element') testConnector.flushAllMessages() t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)') // test childRemoved xml0.delete(0) t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element') testConnector.flushAllMessages() t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)') compare(users) } /** * @param {t.TestCase} tc */ export const testTreewalker = tc => { const { users, xml0 } = init(tc, { users: 3 }) const paragraph1 = new Y.XmlElement('p') const paragraph2 = new Y.XmlElement('p') const text1 = new Y.XmlText('init') const text2 = new Y.XmlText('text') paragraph1.insert(0, [text1, text2]) xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) const allParagraphs = xml0.querySelectorAll('p') t.assert(allParagraphs.length === 2, 'found exactly two paragraphs') t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1') t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2') t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1') compare(users) } /** * @param {t.TestCase} _tc */ export const testYtextAttributes = _tc => { const ydoc = new Y.Doc() const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText)) ytext.observe(event => { t.compare(event.changes.keys.get('test'), { action: 'add', oldValue: undefined }) }) ytext.setAttribute('test', 42) t.compare(ytext.getAttribute('test'), 42) t.compare(ytext.getAttributes(), { test: 42 }) } /** * @param {t.TestCase} _tc */ export const testSiblings = _tc => { const ydoc = new Y.Doc() const yxml = ydoc.getXmlFragment() const first = new Y.XmlText() const second = new Y.XmlElement('p') yxml.insert(0, [first, second]) t.assert(first.nextSibling === second) t.assert(second.prevSibling === first) t.assert(first.parent === yxml) t.assert(yxml.parent === null) t.assert(yxml.firstChild === first) } /** * @param {t.TestCase} _tc */ export const testInsertafter = _tc => { const ydoc = new Y.Doc() const yxml = ydoc.getXmlFragment() const first = new Y.XmlText() const second = new Y.XmlElement('p') const third = new Y.XmlElement('p') const deepsecond1 = new Y.XmlElement('span') const deepsecond2 = new Y.XmlText() second.insertAfter(null, [deepsecond1]) second.insertAfter(deepsecond1, [deepsecond2]) yxml.insertAfter(null, [first, second]) yxml.insertAfter(second, [third]) t.assert(yxml.length === 3) t.assert(second.get(0) === deepsecond1) t.assert(second.get(1) === deepsecond2) t.compareArrays(yxml.toArray(), [first, second, third]) t.fails(() => { const el = new Y.XmlElement('p') el.insertAfter(deepsecond1, [new Y.XmlText()]) }) } /** * @param {t.TestCase} _tc */ export const testClone = _tc => { const ydoc = new Y.Doc() const yxml = ydoc.getXmlFragment() const first = new Y.XmlText('text') const second = new Y.XmlElement('p') const third = new Y.XmlElement('p') yxml.push([first, second, third]) t.compareArrays(yxml.toArray(), [first, second, third]) const cloneYxml = yxml.clone() ydoc.getArray('copyarr').insert(0, [cloneYxml]) t.assert(cloneYxml.length === 3) t.compare(cloneYxml.toJSON(), yxml.toJSON()) } /** * @param {t.TestCase} _tc */ export const testFormattingBug = _tc => { const ydoc = new Y.Doc() const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText)) const delta = [ { insert: 'A', attributes: { em: {}, strong: {} } }, { insert: 'B', attributes: { em: {} } }, { insert: 'C', attributes: { em: {}, strong: {} } } ] yxml.applyDelta(delta) t.compare(yxml.toDelta(), delta) } yjs-13.6.8/tsconfig.json000066400000000000000000000007251450200430400151210ustar00rootroot00000000000000{ "compilerOptions": { "target": "ES2021", "lib": ["ES2021", "dom"], "module": "node16", "allowJs": true, "checkJs": true, "declaration": true, "declarationMap": true, "outDir": "./dist", "baseUrl": "./", "emitDeclarationOnly": true, "strict": true, "noImplicitAny": true, "moduleResolution": "nodenext", "paths": { "yjs": ["./src/index.js"] } }, "include": ["./src/**/*.js", "./tests/**/*.js"] }