Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cb423c046 | ||
|
|
4547b35641 | ||
|
|
4c87f9a021 | ||
|
|
4b08c67e06 | ||
|
|
9f5bc9ddfe | ||
|
|
b399ffa765 | ||
|
|
180f4667c1 | ||
|
|
9455373611 | ||
|
|
aa804d89c0 | ||
|
|
3ef51a5d1a | ||
|
|
e61089c659 | ||
|
|
97625cf29b | ||
|
|
a5dc6c27aa |
31
.github/workflows/nodejs.yml
vendored
Normal file
31
.github/workflows/nodejs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 13.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
|
||||
39
README.md
39
README.md
@@ -52,13 +52,10 @@ are implemented in separate modules.
|
||||
|
||||
| Name | Cursors | Binding | Demo |
|
||||
|---|:-:|---|---|
|
||||
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
|
||||
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
|
||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
|
||||
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
|
||||
| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/yjs/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) |
|
||||
| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/yjs/y-dom) | [demo](https://yjs-demos.now.sh/dom/) |
|
||||
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) |
|
||||
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
|
||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||
|
||||
### Providers
|
||||
|
||||
@@ -466,6 +463,12 @@ const doc = new Y.Doc()
|
||||
<dl>
|
||||
<b><code>clientID</code></b>
|
||||
<dd>A unique id that identifies this client. (readonly)</dd>
|
||||
<b><code>gc</code></b>
|
||||
<dd>
|
||||
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.
|
||||
</dd>
|
||||
<b><code>transact(function(Transaction):void [, origin:any])</code></b>
|
||||
<dd>
|
||||
Every change on the shared document happens in a transaction. Observer calls and
|
||||
@@ -653,8 +656,8 @@ ytext.toString() // => 'abc'
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>constructor(scope:Y.AbstractType|Array<Y.AbstractType>,
|
||||
[[{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}]])</code></b>
|
||||
<b><code>constructor(scope:Y.AbstractType|Array<Y.AbstractType>
|
||||
[, {captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}])</code></b>
|
||||
<dd>Accepts either single type as scope or an array of types.</dd>
|
||||
<b><code>undo()</code></b>
|
||||
<dd></dd>
|
||||
@@ -694,28 +697,30 @@ StackItem won't be merged.
|
||||
// without stopCapturing
|
||||
ytext.insert(0, 'a')
|
||||
ytext.insert(1, 'b')
|
||||
um.undo()
|
||||
undoManager.undo()
|
||||
ytext.toString() // => '' (note that 'ab' was removed)
|
||||
// with stopCapturing
|
||||
ytext.insert(0, 'a')
|
||||
um.stopCapturing()
|
||||
undoManager.stopCapturing()
|
||||
ytext.insert(0, 'b')
|
||||
um.undo()
|
||||
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 `trackedTransactionOrigins` you can
|
||||
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 `trackedTransactionOrigins`.
|
||||
UndoManager instance is always added to `trackedOrigins`.
|
||||
|
||||
```js
|
||||
class CustomBinding {}
|
||||
|
||||
const ytext = doc.getArray('array')
|
||||
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
|
||||
const undoManager = new Y.UndoManager(ytext, {
|
||||
trackedOrigins: new Set([42, CustomBinding])
|
||||
})
|
||||
|
||||
ytext.insert(0, 'abc')
|
||||
undoManager.undo()
|
||||
@@ -753,7 +758,9 @@ document. You can assign meta-information to Undo-/Redo-StackItems.
|
||||
|
||||
```js
|
||||
const ytext = doc.getArray('array')
|
||||
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
|
||||
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
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.5",
|
||||
"version": "13.0.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1439,9 +1439,9 @@
|
||||
}
|
||||
},
|
||||
"lib0": {
|
||||
"version": "0.2.22",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.22.tgz",
|
||||
"integrity": "sha512-SFzX7/SGgrOK6VABQugczhAwaaZLW1VcrE9xG+cVG1+AMQWmcu/7SZaJq0KORnfHr1xK4P6JUBWfoxSCwBcgLA==",
|
||||
"version": "0.2.26",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.26.tgz",
|
||||
"integrity": "sha512-DTf0VmFNi/eT+3Q+6rNHYdIAx69ROpvQkpnplpDoErW8NeRwjPwoIKjCF3rKebsMrQoxH4tFD1bvMQb4CUzcFg==",
|
||||
"requires": {
|
||||
"isomorphic.js": "^0.1.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.5",
|
||||
"version": "13.0.7",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"homepage": "https://yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.22"
|
||||
"lib0": "^0.2.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^11.0.1",
|
||||
|
||||
@@ -26,6 +26,7 @@ export {
|
||||
ContentType,
|
||||
AbstractType,
|
||||
RelativePosition,
|
||||
getTypeChildren,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
@@ -47,6 +48,7 @@ export {
|
||||
typeMapGetSnapshot,
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
readUpdate,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateVector,
|
||||
UndoManager,
|
||||
|
||||
@@ -19,6 +19,22 @@ import * as iterator from 'lib0/iterator.js'
|
||||
import * as error from 'lib0/error.js'
|
||||
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* Accumulate all (list) children of a type and return them as an Array.
|
||||
*
|
||||
* @param {AbstractType<any>} t
|
||||
* @return {Array<Item>}
|
||||
*/
|
||||
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).
|
||||
|
||||
@@ -14,15 +14,19 @@ import {
|
||||
callTypeObservers,
|
||||
transact,
|
||||
ContentEmbed,
|
||||
GC,
|
||||
ContentFormat,
|
||||
ContentString,
|
||||
splitSnapshotAffectedStructs,
|
||||
iterateDeletedStructs,
|
||||
iterateStructs,
|
||||
ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
import * as object from 'lib0/object.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
|
||||
/**
|
||||
* @param {any} a
|
||||
@@ -320,6 +324,110 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
return insertNegatedAttributes(transaction, parent, left, right, 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} end exclusive end, automatically iterates to the next Content Item
|
||||
* @param {Map<string,any>} startAttributes
|
||||
* @param {Map<string,any>} endAttributes This attribute is modified!
|
||||
* @return {number} The amount of formatting Items deleted.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
|
||||
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
|
||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
let cleanups = 0
|
||||
while (start !== end) {
|
||||
if (!start.deleted) {
|
||||
const content = start.content
|
||||
switch (content.constructor) {
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||
if ((endAttributes.get(key) || null) !== value || (startAttributes.get(key) || null) === value) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
cleanups++
|
||||
}
|
||||
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.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) {
|
||||
item = item.right
|
||||
}
|
||||
const attrs = new Set()
|
||||
// iterate back until a content item is found
|
||||
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) {
|
||||
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
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
|
||||
startAttributes = map.copy(currentAttributes)
|
||||
start = end
|
||||
break
|
||||
}
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item|null} left
|
||||
@@ -332,6 +440,8 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
|
||||
* @function
|
||||
*/
|
||||
const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||
const startAttrs = map.copy(currentAttributes)
|
||||
const start = right
|
||||
while (length > 0 && right !== null) {
|
||||
if (right.deleted === false) {
|
||||
switch (right.content.constructor) {
|
||||
@@ -351,6 +461,9 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||
left = right
|
||||
right = right.right
|
||||
}
|
||||
if (start) {
|
||||
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
|
||||
}
|
||||
return { left, right }
|
||||
}
|
||||
|
||||
@@ -649,7 +762,48 @@ export class YText extends AbstractType {
|
||||
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs) {
|
||||
callTypeObservers(this, transaction, new YTextEvent(this, transaction))
|
||||
const event = new YTextEvent(this, transaction)
|
||||
const doc = transaction.doc
|
||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||
if (!transaction.local) {
|
||||
// check if another formatting item was inserted
|
||||
let foundFormattingItem = false
|
||||
for (const [client, afterClock] of transaction.afterState) {
|
||||
const clock = transaction.beforeState.get(client) || 0
|
||||
if (afterClock === clock) {
|
||||
continue
|
||||
}
|
||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||
// @ts-ignore
|
||||
if (!item.deleted && item.content.constructor === ContentFormat) {
|
||||
foundFormattingItem = true
|
||||
}
|
||||
})
|
||||
if (foundFormattingItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
transact(doc, t => {
|
||||
if (foundFormattingItem) {
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
cleanupYTextFormatting(this)
|
||||
} else {
|
||||
// If no formatting attribute was inserted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
iterateDeletedStructs(t, transaction.deleteSet, item => {
|
||||
if (item instanceof GC) {
|
||||
return
|
||||
}
|
||||
if (item.parent === this) {
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
callTypeObservers(this, transaction, event)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,6 +1070,9 @@ export class YText extends AbstractType {
|
||||
* @public
|
||||
*/
|
||||
format (index, length, attributes) {
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
|
||||
@@ -17,6 +17,8 @@ import { Observable } from 'lib0/observable.js'
|
||||
import * as random from 'lib0/random.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
|
||||
export const generateNewClientId = random.uint32
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
* @extends Observable<string>
|
||||
@@ -31,7 +33,7 @@ export class Doc extends Observable {
|
||||
super()
|
||||
this.gc = gc
|
||||
this.gcFilter = gcFilter
|
||||
this.clientID = random.uint32()
|
||||
this.clientID = generateNewClientId()
|
||||
/**
|
||||
* @type {Map<string, AbstractType<YEvent>>}
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
findIndexSS,
|
||||
callEventHandlerListeners,
|
||||
Item,
|
||||
generateNewClientId,
|
||||
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -17,6 +18,7 @@ import * as encoding from 'lib0/encoding.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as set from 'lib0/set.js'
|
||||
import * as logging from 'lib0/logging.js'
|
||||
import { callAll } from 'lib0/function.js'
|
||||
|
||||
/**
|
||||
@@ -313,6 +315,10 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
||||
tryToMergeWithLeft(structs, replacedStructPos)
|
||||
}
|
||||
}
|
||||
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
||||
doc.clientID = generateNewClientId()
|
||||
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
|
||||
}
|
||||
// @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')) {
|
||||
|
||||
19
tests/consistency.tests.js
Normal file
19
tests/consistency.tests.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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 consistency from './consistency.tests.js'
|
||||
|
||||
import { runTests } from 'lib0/testing.js'
|
||||
import { isBrowser, isNode } from 'lib0/environment.js'
|
||||
@@ -14,7 +15,7 @@ if (isBrowser) {
|
||||
log.createVConsole(document.body)
|
||||
}
|
||||
runTests({
|
||||
map, array, text, xml, encoding, undoredo
|
||||
map, array, text, xml, consistency, encoding, undoredo
|
||||
}).then(success => {
|
||||
/* istanbul ignore next */
|
||||
if (isNode) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import * as Y from './testHelper.js'
|
||||
import * as t from 'lib0/testing.js'
|
||||
import * as prng from 'lib0/prng.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
|
||||
const { init, compare } = Y
|
||||
|
||||
/**
|
||||
@@ -180,3 +183,189 @@ export const testToDeltaEmbedNoAttributes = tc => {
|
||||
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)
|
||||
}
|
||||
|
||||
// RANDOM TESTS
|
||||
|
||||
let charCounter = 0
|
||||
|
||||
const marks = [
|
||||
{ bold: true },
|
||||
{ italic: true },
|
||||
{ italic: true, color: '#888' }
|
||||
]
|
||||
|
||||
const marksChoices = [
|
||||
undefined,
|
||||
...marks
|
||||
]
|
||||
|
||||
/**
|
||||
* @type Array<function(any,prng.PRNG):void>
|
||||
*/
|
||||
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.toString().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.toString().length)
|
||||
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
|
||||
},
|
||||
/**
|
||||
* @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 {any} result
|
||||
*/
|
||||
const checkResult = result => {
|
||||
for (let i = 1; i < result.testObjects.length; i++) {
|
||||
const p1 = result.users[i].getText('text').toDelta()
|
||||
const p2 = result.users[i].getText('text').toDelta()
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user