From da748a78f41a9fd40ddcbe77a0c0256234c18c9c Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 15 Feb 2018 01:25:08 +0100 Subject: [PATCH 01/22] start rewriting y-richtext --- src/Struct/Item.js | 7 ++ src/Struct/ItemFormat.js | 45 +++++++++ src/Type/YArray.js | 8 +- src/Type/YText.js | 202 ++++++++++++++++++++++++++++++++------- 4 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 src/Struct/ItemFormat.js diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 27f4224b..96a75cc8 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -73,6 +73,13 @@ export default class Item { get _length () { return 1 } + /** + * Some elements are not supposed to be addressable. For example, an + * ItemFormat should not be retrievable via yarray.get(pos) + */ + get _countable () { + return true + } /** * Splits this struct so that another struct can be inserted in-between. * This must be overwritten if _length > 1 diff --git a/src/Struct/ItemFormat.js b/src/Struct/ItemFormat.js new file mode 100644 index 00000000..de33f8f5 --- /dev/null +++ b/src/Struct/ItemFormat.js @@ -0,0 +1,45 @@ +import { default as Item } from './Item.js' +import { logID } from '../MessageHandler/messageToString.js' + +export default class ItemString extends Item { + constructor () { + super() + this.key = null + this.value = null + } + _copy (undeleteChildren, copyPosition) { + let struct = super._copy(undeleteChildren, copyPosition) + struct.key = this.key + struct.value = this.value + return struct + } + get _length () { + return 1 + } + get _countable () { + return false + } + _fromBinary (y, decoder) { + let missing = super._fromBinary(y, decoder) + this.key = decoder.readVarString() + this.value = decoder.readVarString() + return missing + } + _toBinary (encoder) { + super._toBinary(encoder) + encoder.writeVarString(this.key) + encoder.writeVarString(this.value) + } + _logString () { + const left = this._left !== null ? this._left._lastId : null + const origin = this._origin !== null ? this._origin._lastId : null + return `ItemFormat(id:${logID(this._id)},key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` + } + _splitAt (y, diff) { + if (diff === 0) { + return this + } else { + return this._right + } + } +} diff --git a/src/Type/YArray.js b/src/Type/YArray.js index 16c64034..886af1af 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -45,7 +45,7 @@ export default class YArray extends Type { get (pos) { let n = this._start while (n !== null) { - if (!n._deleted) { + if (!n._deleted && n._countable) { if (pos < n._length) { if (n.constructor === ItemJSON || n.constructor === ItemString) { return n._content[pos] @@ -84,7 +84,7 @@ export default class YArray extends Type { let pos = 0 let n = this._start while (n !== null) { - if (!n._deleted) { + if (!n._deleted && n._countable) { if (n instanceof Type) { f(n, pos++, this) } else { @@ -103,7 +103,7 @@ export default class YArray extends Type { let length = 0 let n = this._start while (n !== null) { - if (!n._deleted) { + if (!n._deleted && n._countable) { length += n._length } n = n._right @@ -144,7 +144,7 @@ export default class YArray extends Type { let item = this._start let count = 0 while (item !== null && length > 0) { - if (!item._deleted) { + if (!item._deleted && item._countable) { if (count <= pos && pos < count + item._length) { const diffDel = pos - count item = item._splitAt(this._y, diffDel) diff --git a/src/Type/YText.js b/src/Type/YText.js index 959b92fe..02650ea2 100644 --- a/src/Type/YText.js +++ b/src/Type/YText.js @@ -1,7 +1,66 @@ import ItemString from '../Struct/ItemString.js' +import ItemFormat from '../Struct/ItemFormat.js' import YArray from './YArray.js' import { logID } from '../MessageHandler/messageToString.js' +function integrateItem (item, parent, y, left, right) { + item._origin = left + item._left = left + item._right = right + item._right_origin = right + item._parent = parent + if (y !== null) { + item._integrate(this._y) + } else if (left === null) { + parent._start = item + } else { + left._right = item + } +} + +function findPosition (parent, pos, attributes) { + let currentAttributes = new Map() + let left = null + let right = parent._start + let count = 0 + while (right !== null) { + switch (right.constructor) { + // case ItemBlockFormat: do not break.. + case ItemString: + const rightLen = right._deleted ? 0 : (right._length - 1) + if (count <= pos && pos <= count + rightLen) { + const splitDiff = pos - count + right = right._splitAt(parent._y, splitDiff) + left = right._left + count += splitDiff + break + } + if (!right._deleted) { + count += right._length + } + break + case ItemFormat: + if (right._deleted === false) { + const key = right.key + const value = right.value + if (value === null) { + currentAttributes.delete(key) + } else if (attributes.hasOwnProperty(key)) { + // only set if relevant + currentAttributes.set(key, value) + } + } + break + } + left = right + right = right._right + } + if (pos > count) { + throw new Error('Position exceeds array range!') + } + return [left, right, currentAttributes] +} + export default class YText extends YArray { constructor (string) { super() @@ -13,55 +72,128 @@ export default class YText extends YArray { } } toString () { - const strBuilder = [] + let str = '' let n = this._start while (n !== null) { - if (!n._deleted) { - strBuilder.push(n._content) + if (!n._deleted && n._countable) { + str += n._content } n = n._right } - return strBuilder.join('') + return str } - insert (pos, text) { + /** + * As defined by Quilljs - https://quilljs.com/docs/delta/ + */ + toRichtextDelta () { + let ops = [] + let currentAttributes = new Map() + let str = '' + let n = this._start + function packStr () { + if (str.length > 0) { + // pack str with attributes to ops + let attributes = {} + for (let [key, value] of currentAttributes) { + attributes[key] = value + } + ops.push({ insert: str, attributes }) + str = '' + } + } + while (n !== null) { + if (!n._deleted) { + switch (n.constructor) { + case ItemString: + str += n._content + break + case ItemFormat: + packStr() + const value = n.value + const key = n.key + if (value === null) { + currentAttributes.delete(key) + } else { + currentAttributes.set(key, value) + } + break + } + } + n = n._right + } + packStr() + return ops + } + insert (pos, text, attributes = {}) { if (text.length <= 0) { return } this._transact(y => { - let left = null - let right = this._start - let count = 0 - while (right !== null) { - const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= pos && pos <= count + rightLen) { - const splitDiff = pos - count - right = right._splitAt(this._y, splitDiff) - left = right._left - count += splitDiff - break + let [left, right, currentAttributes] = findPosition(this, pos, attributes) + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, this, y, left, right) + left = format } - if (!right._deleted) { - count += right._length - } - left = right - right = right._right - } - if (pos > count) { - throw new Error('Position exceeds array range!') } + // insert text content let item = new ItemString() - item._origin = left - item._left = left - item._right = right - item._right_origin = right - item._parent = this item._content = text - if (y !== null) { - item._integrate(this._y) - } else if (left === null) { - this._start = item - } else { - left._right = item + integrateItem(item, this, y, left, right) + left = item + // negate applied formats + for (let [key, value] of negatedAttributes) { + let format = new ItemFormat() + format.key = key + format.value = value + integrateItem(format, this, y, left, right) + left = format + } + }) + } + format (pos, length, attributes) { + this._transact(y => { + let [left, _right, currentAttributes] = findPosition(this, pos, attributes) + if (_right === null) { + return + } + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, this, y, left, _right) + left = format + } + } + // iterate until first non-format or null is found + // delete all formats with attributes[format.key] != null + while (length > 0 && left !== null) { + if (left._deleted === false) { + if (left.constructor === ItemFormat) { + if (attributes[left.key] != null) { + left.delete(y) + } + } else if (length < left._length) { + + } + } + left = left._right } }) } From 248d08be306411a5d432976f9db338508b913a84 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 26 Feb 2018 02:18:39 +0100 Subject: [PATCH 02/22] implement quill binding for y-text --- examples/quill/index.html | 26 +- examples/quill/index.js | 61 +-- package-lock.json | 973 +++-------------------------------- package.json | 1 + rollup.browser.js | 4 +- rollup.test.js | 2 +- src/Binary/Decoder.js | 2 +- src/Binding/QuillBinding.js | 37 ++ src/Struct/Item.js | 5 + src/Struct/ItemEmbed.js | 31 ++ src/Struct/ItemFormat.js | 15 +- src/Type/YArray.js | 24 +- src/Type/YText.js | 492 ++++++++++++++---- src/Util/structReferences.js | 5 + src/Y.js | 2 + test/y-text.tests.js | 102 ++++ tests-lib/helper.js | 21 +- 17 files changed, 713 insertions(+), 1090 deletions(-) create mode 100644 src/Binding/QuillBinding.js create mode 100644 src/Struct/ItemEmbed.js create mode 100644 test/y-text.tests.js diff --git a/examples/quill/index.html b/examples/quill/index.html index 4a8128b3..0f881d27 100644 --- a/examples/quill/index.html +++ b/examples/quill/index.html @@ -1,32 +1,18 @@ - - - - - - + + + + + +
- - - - - - - diff --git a/examples/quill/index.js b/examples/quill/index.js index f472cb7b..8e2ab6e9 100644 --- a/examples/quill/index.js +++ b/examples/quill/index.js @@ -1,40 +1,33 @@ /* global Y, Quill */ -// initialize a shared object. This function call returns a promise! - -Y({ - db: { - name: 'memory' - }, +let y = new Y('htmleditor10', { connector: { name: 'websockets-client', - room: 'richtext-example-quill-1.0-test', - url: 'http://localhost:1234' - }, - sourceDir: '/bower_components', - share: { - richtext: 'Richtext' // y.share.richtext is of type Y.Richtext + url: 'http://127.0.0.1:1234' } -}).then(function (y) { - window.yQuill = y - - // create quill element - window.quill = new Quill('#quill', { - modules: { - formula: true, - syntax: true, - toolbar: [ - [{ size: ['small', false, 'large', 'huge'] }], - ['bold', 'italic', 'underline'], - [{ color: [] }, { background: [] }], // Snow theme fills in values - [{ script: 'sub' }, { script: 'super' }], - ['link', 'image'], - ['link', 'code-block'], - [{ list: 'ordered' }] - ] - }, - theme: 'snow' - }) - // bind quill to richtext type - y.share.richtext.bind(window.quill) }) + +let quill = new Quill('#quill-container', { + modules: { + toolbar: [ + [{ header: [1, 2, false] }], + ['bold', 'italic', 'underline'], + ['image', 'code-block'], + [{ color: [] }, { background: [] }], // Snow theme fills in values + [{ script: 'sub' }, { script: 'super' }], + ['link', 'image'], + ['link', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }] + ] + }, + placeholder: 'Compose an epic...', + theme: 'snow' // or 'bubble' +}) + +let yText = y.define('quill', Y.Text) + +let quillBinding = new Y.QuillBinding(yText, quill) +window.quillBinding = quillBinding +window.yText = yText +window.y = y +window.quill = quill diff --git a/package-lock.json b/package-lock.json index d698c8a1..8bc1546c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,7 +1039,6 @@ "requires": { "anymatch": "1.3.0", "async-each": "1.0.1", - "fsevents": "1.1.3", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1080,6 +1079,12 @@ "wordwrap": "0.0.2" } }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1310,6 +1315,12 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1777,6 +1788,12 @@ "es5-ext": "0.10.23" } }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=", + "dev": true + }, "exit-hook": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", @@ -1810,6 +1827,12 @@ "os-homedir": "1.0.2" } }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -1834,6 +1857,12 @@ "integrity": "sha1-ysNCuPqJAm7+c6Jg/p9rgE9J5H8=", "dev": true }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -1991,910 +2020,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, "function-bind": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", @@ -3843,13 +2968,6 @@ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", - "dev": true, - "optional": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4012,6 +3130,12 @@ "p-limit": "1.1.0" } }, + "parchment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.3.tgz", + "integrity": "sha512-41Y+F8FejGa+URCuDTlS1zzzlYCwoZFTWpVwiQWDL82LFAAlIIiAo3JGJSLMiSPDeM3avFUivdXN3iY/i4mBXg==", + "dev": true + }, "parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", @@ -4199,6 +3323,31 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "quill": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.5.tgz", + "integrity": "sha512-08P1DqKz4OZPJSlwSiZQxQ1a0F56+KEz6MttlpDNE42+WpjGuOyvsEQepScpdeyilHWrQwh61M5C1KelP8I8IA==", + "dev": true, + "requires": { + "clone": "2.1.1", + "deep-equal": "1.0.1", + "eventemitter3": "2.0.3", + "extend": "3.0.1", + "parchment": "1.1.3", + "quill-delta": "3.6.2" + } + }, + "quill-delta": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.2.tgz", + "integrity": "sha512-grWEQq9woEidPDogtDNxQKmy2LFf9zBC0EU/YTSw6TwKmMjtihTxdnPtPRfrqazB2MSJ7YdCWxmsJ7aQKRSEgg==", + "dev": true, + "requires": { + "deep-equal": "1.0.1", + "extend": "3.0.1", + "fast-diff": "1.1.2" + } + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", diff --git a/package.json b/package.json index d50e4c54..2e5cbd06 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "chance": "^1.0.9", "concurrently": "^3.4.0", "cutest": "^0.1.9", + "quill": "^1.3.5", "rollup-plugin-babel": "^2.7.1", "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-inject": "^2.0.0", diff --git a/rollup.browser.js b/rollup.browser.js index 53149fdf..dfea0f74 100644 --- a/rollup.browser.js +++ b/rollup.browser.js @@ -20,7 +20,7 @@ export default { }), commonjs(), babel(), - uglify({ + /*uglify({ mangle: { except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item'] }, @@ -34,7 +34,7 @@ export default { } } } - }) + })*/ ], banner: ` /** diff --git a/rollup.test.js b/rollup.test.js index f4072276..7e948a42 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' import multiEntry from 'rollup-plugin-multi-entry' export default { - input: 'test/index.js', + input: 'test/y-text.tests.js', name: 'y-tests', sourcemap: true, output: { diff --git a/src/Binary/Decoder.js b/src/Binary/Decoder.js index 7208c4dd..fcc18b72 100644 --- a/src/Binary/Decoder.js +++ b/src/Binary/Decoder.js @@ -90,7 +90,7 @@ export default class BinaryDecoder { for (let i = 0; i < len; i++) { bytes[i] = this.uint8arr[this.pos++] } - let encodedString = String.fromCodePoint(...bytes) + let encodedString = bytes.map(b => String.fromCodePoint(b)).join('') return decodeURIComponent(escape(encodedString)) } /** diff --git a/src/Binding/QuillBinding.js b/src/Binding/QuillBinding.js new file mode 100644 index 00000000..8920f20f --- /dev/null +++ b/src/Binding/QuillBinding.js @@ -0,0 +1,37 @@ + +import Binding from './Binding.js' + +function typeObserver (event) { + const quill = this.target + quill.update('yjs') + this._mutualExclude(function () { + quill.updateContents(event.delta, 'yjs') + quill.update('yjs') // ignore applied changes + }) +} + +function quillObserver (delta) { + this._mutualExclude(() => { + this.type.applyDelta(delta.ops) + }) +} + +export default class QuillBinding extends Binding { + constructor (textType, quillInstance) { + // Binding handles textType as this.type and quillInstance as this.target + super(textType, quillInstance) + // set initial value + quillInstance.setContents(textType.toDelta(), 'yjs') + // Observers are handled by this class + this._typeObserver = typeObserver.bind(this) + this._quillObserver = quillObserver.bind(this) + textType.observe(this._typeObserver) + quillInstance.on('text-change', this._quillObserver) + } + destroy () { + // Remove everything that is handled by this class + this.type.unobserve(this._typeObserver) + this.target.unobserve(this._quillObserver) + super.destroy() + } +} diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 96a75cc8..d6fd420b 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -39,6 +39,11 @@ export function splitHelper (y, a, b, diff) { o = o._right } y.os.put(b) + if (y._transaction.newTypes.has(a)) { + y._transaction.newTypes.add(b) + } else if (y._transaction.deletedStructs.has(a)) { + y._transaction.deletedStructs.add(b) + } } export default class Item { diff --git a/src/Struct/ItemEmbed.js b/src/Struct/ItemEmbed.js new file mode 100644 index 00000000..22c9fc2f --- /dev/null +++ b/src/Struct/ItemEmbed.js @@ -0,0 +1,31 @@ +import { default as Item } from './Item.js' +import { logID } from '../MessageHandler/messageToString.js' + +export default class ItemEmbed extends Item { + constructor () { + super() + this.embed = null + } + _copy (undeleteChildren, copyPosition) { + let struct = super._copy(undeleteChildren, copyPosition) + struct.embed = this.embed + return struct + } + get _length () { + return 1 + } + _fromBinary (y, decoder) { + const missing = super._fromBinary(y, decoder) + this.embed = JSON.parse(decoder.readVarString()) + return missing + } + _toBinary (encoder) { + super._toBinary(encoder) + encoder.writeVarString(JSON.stringify(this.embed)) + } + _logString () { + const left = this._left !== null ? this._left._lastId : null + const origin = this._origin !== null ? this._origin._lastId : null + return `ItemEmbed(id:${logID(this._id)},embed:${JSON.stringify(this.embed)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` + } +} diff --git a/src/Struct/ItemFormat.js b/src/Struct/ItemFormat.js index de33f8f5..b384406b 100644 --- a/src/Struct/ItemFormat.js +++ b/src/Struct/ItemFormat.js @@ -1,7 +1,7 @@ import { default as Item } from './Item.js' import { logID } from '../MessageHandler/messageToString.js' -export default class ItemString extends Item { +export default class ItemFormat extends Item { constructor () { super() this.key = null @@ -20,26 +20,19 @@ export default class ItemString extends Item { return false } _fromBinary (y, decoder) { - let missing = super._fromBinary(y, decoder) + const missing = super._fromBinary(y, decoder) this.key = decoder.readVarString() - this.value = decoder.readVarString() + this.value = JSON.parse(decoder.readVarString()) return missing } _toBinary (encoder) { super._toBinary(encoder) encoder.writeVarString(this.key) - encoder.writeVarString(this.value) + encoder.writeVarString(JSON.stringify(this.value)) } _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null return `ItemFormat(id:${logID(this._id)},key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` } - _splitAt (y, diff) { - if (diff === 0) { - return this - } else { - return this._right - } - } } diff --git a/src/Type/YArray.js b/src/Type/YArray.js index 886af1af..90835566 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -4,12 +4,13 @@ import ItemString from '../Struct/ItemString.js' import { logID } from '../MessageHandler/messageToString.js' import YEvent from '../Util/YEvent.js' -class YArrayEvent extends YEvent { +export class YArrayEvent extends YEvent { constructor (yarray, remote, transaction) { super(yarray) this.remote = remote this._transaction = transaction this._addedElements = null + this._removedElements = null } get addedElements () { if (this._addedElements === null) { @@ -26,15 +27,18 @@ class YArrayEvent extends YEvent { return this._addedElements } get removedElements () { - const target = this.target - const transaction = this._transaction - const removedElements = new Set() - transaction.deletedStructs.forEach(function (struct) { - if (struct._parent === target && !transaction.newTypes.has(struct)) { - removedElements.add(struct) - } - }) - return removedElements + if (this._removedElements === null) { + const target = this.target + const transaction = this._transaction + const removedElements = new Set() + transaction.deletedStructs.forEach(function (struct) { + if (struct._parent === target && !transaction.newTypes.has(struct)) { + removedElements.add(struct) + } + }) + this._removedElements = removedElements + } + return this._removedElements } } diff --git a/src/Type/YText.js b/src/Type/YText.js index 02650ea2..926d8ef7 100644 --- a/src/Type/YText.js +++ b/src/Type/YText.js @@ -1,7 +1,8 @@ import ItemString from '../Struct/ItemString.js' +import ItemEmbed from '../Struct/ItemEmbed.js' import ItemFormat from '../Struct/ItemFormat.js' -import YArray from './YArray.js' import { logID } from '../MessageHandler/messageToString.js' +import { YArrayEvent, default as YArray } from './YArray.js' function integrateItem (item, parent, y, left, right) { item._origin = left @@ -10,7 +11,7 @@ function integrateItem (item, parent, y, left, right) { item._right_origin = right item._parent = parent if (y !== null) { - item._integrate(this._y) + item._integrate(y) } else if (left === null) { parent._start = item } else { @@ -18,49 +19,365 @@ function integrateItem (item, parent, y, left, right) { } } -function findPosition (parent, pos, attributes) { - let currentAttributes = new Map() - let left = null - let right = parent._start - let count = 0 - while (right !== null) { +function findNextPosition (currentAttributes, parent, left, right, count) { + while (right !== null && count > 0) { switch (right.constructor) { - // case ItemBlockFormat: do not break.. + case ItemEmbed: case ItemString: const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= pos && pos <= count + rightLen) { - const splitDiff = pos - count - right = right._splitAt(parent._y, splitDiff) + if (count <= rightLen) { + right = right._splitAt(parent._y, count) left = right._left - count += splitDiff - break + return [left, right, currentAttributes] } - if (!right._deleted) { - count += right._length + if (right._deleted === false) { + count -= right._length } break case ItemFormat: if (right._deleted === false) { - const key = right.key - const value = right.value - if (value === null) { - currentAttributes.delete(key) - } else if (attributes.hasOwnProperty(key)) { - // only set if relevant - currentAttributes.set(key, value) - } + updateCurrentAttributes(currentAttributes, right) } break } left = right right = right._right } - if (pos > count) { - throw new Error('Position exceeds array range!') - } return [left, right, currentAttributes] } +function findPosition (parent, pos) { + let currentAttributes = new Map() + let left = null + let right = parent._start + return findNextPosition(currentAttributes, parent, left, right, pos) +} + +// negate applied formats +function insertNegatedAttributes (y, parent, left, right, negatedAttributes) { + // check if we really need to remove attributes + while ( + right !== null && ( + right._deleted === true || ( + right.constructor === ItemFormat && + (negatedAttributes.get(right.key) === right.value) + ) + ) + ) { + if (right._deleted === false) { + negatedAttributes.delete(right.key) + } + left = right + right = right._right + } + for (let [key, val] of negatedAttributes) { + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + return [left, right] +} + +function updateCurrentAttributes (currentAttributes, item) { + const value = item.value + const key = item.key + if (value === null) { + currentAttributes.delete(key) + } else { + currentAttributes.set(key, value) + } +} + +function minimizeAttributeChanges (left, right, currentAttributes, attributes) { + // go right while attributes[right.key] === right.value (or right is deleted) + while (true) { + if (right === null) { + break + } else if (right._deleted === true) { + // continue + } else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) { + // found a format, update currentAttributes and continue + updateCurrentAttributes(currentAttributes, right) + } else { + break + } + left = right + right = right._right + } + return [left, right] +} + +function insertText (y, text, parent, left, right, currentAttributes, attributes) { + for (let [key] of currentAttributes) { + if (attributes.hasOwnProperty(key) === false) { + attributes[key] = null + } + } + [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + } + // insert content + let item + if (text.constructor === String) { + item = new ItemString() + item._content = text + } else { + item = new ItemEmbed() + item.embed = text + } + integrateItem(item, parent, y, left, right) + left = item + return insertNegatedAttributes(y, parent, left, right, negatedAttributes) +} + +function formatText (y, length, parent, left, right, currentAttributes, attributes) { + [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + } + // iterate until first non-format or null is found + // delete all formats with attributes[format.key] != null + while (length > 0 && right !== null) { + if (right._deleted === false) { + switch (right.constructor) { + case ItemFormat: + if (attributes.hasOwnProperty(right.key)) { + if (attributes[right.key] === right.value) { + negatedAttributes.delete(right.key) + } else { + negatedAttributes.set(right.key, right.value) + } + right._delete(y) + } + updateCurrentAttributes(currentAttributes, right) + break + case ItemEmbed: + case ItemString: + right._splitAt(y, length) + length -= right._length + break + } + } + left = right + right = right._right + } + return insertNegatedAttributes(y, parent, left, right, negatedAttributes) +} + +function deleteText (y, length, parent, left, right, currentAttributes) { + while (length > 0 && right !== null) { + if (right._deleted === false) { + switch (right.constructor) { + case ItemFormat: + updateCurrentAttributes(currentAttributes, right) + break + case ItemEmbed: + case ItemString: + right._splitAt(y, length) + length -= right._length + right._delete(y) + break + } + } + left = right + right = right._right + } + return [left, right] +} + +class YTextEvent extends YArrayEvent { + constructor (ytext, remote, transaction) { + super(ytext, remote, transaction) + this._delta = null + } + get delta () { + if (this._delta === null) { + const y = this.target._y + y.transact(() => { + let item = this.target._start + const delta = [] + const added = this.addedElements + const removed = this.removedElements + this._delta = delta + let action = null + let attributes = {} // counts added or removed new attributes for retain + const currentAttributes = new Map() // saves all current attributes for insert + const oldAttributes = new Map() + let insert = '' + let retain = 0 + let deleteLen = 0 + const addOp = function addOp () { + if (action !== null) { + let op + switch (action) { + case 'delete': + op = { delete: deleteLen } + deleteLen = 0 + break + case 'insert': + op = { insert } + if (currentAttributes.size > 0) { + op.attributes = {} + for (let [key, value] of currentAttributes) { + if (value !== null) { + op.attributes[key] = value + } + } + } + insert = '' + break + case 'retain': + op = { retain } + if (Object.keys(attributes).length > 0) { + op.attributes = {} + for (let key in attributes) { + op.attributes[key] = attributes[key] + } + } + retain = 0 + break + } + delta.push(op) + action = null + } + } + while (item !== null) { + switch (item.constructor) { + case ItemEmbed: + if (added.has(item)) { + addOp() + action = 'insert' + insert = item.embed + addOp() + } else if (removed.has(item)) { + if (action !== 'delete') { + addOp() + action = 'delete' + } + deleteLen += 1 + } else if (item._deleted === false) { + if (action !== 'retain') { + addOp() + action = 'retain' + } + retain += 1 + } + break + case ItemString: + if (added.has(item)) { + if (action !== 'insert') { + addOp() + action = 'insert' + } + insert += item._content + } else if (removed.has(item)) { + if (action !== 'delete') { + addOp() + action = 'delete' + } + deleteLen += item._length + } else if (item._deleted === false) { + if (action !== 'retain') { + addOp() + action = 'retain' + } + retain += item._length + } + break + case ItemFormat: + if (added.has(item)) { + const curVal = currentAttributes.get(item.key) || null + if (curVal !== item.value) { + if (action === 'retain') { + addOp() + } + if (item.value === (oldAttributes.get(item.key) || null)) { + delete attributes[item.key] + } else { + attributes[item.key] = item.value + } + } else { + item._delete(y) + } + } else if (removed.has(item)) { + oldAttributes.set(item.key, item.value) + const curVal = currentAttributes.get(item.key) || null + if (curVal !== item.value) { + if (action === 'retain') { + addOp() + } + attributes[item.key] = curVal + } + } else if (item._deleted === false) { + oldAttributes.set(item.key, item.value) + if (attributes.hasOwnProperty(item.key)) { + if (attributes[item.key] !== item.value) { + if (action === 'retain') { + addOp() + } + if (item.value === null) { + attributes[item.key] = item.value + } else { + delete attributes[item.key] + } + } else { + item._delete(y) + } + } + } + if (item._deleted === false) { + if (action === 'insert') { + addOp() + } + updateCurrentAttributes(currentAttributes, item) + } + break + } + item = item._right + } + addOp() + while (this._delta.length > 0) { + let lastOp = this._delta[this._delta.length - 1] + if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) { + // retain delta's if they don't assign attributes + this._delta.pop() + } else { + break + } + } + }) + } + return this._delta + } +} + export default class YText extends YArray { constructor (string) { super() @@ -71,6 +388,9 @@ export default class YText extends YArray { this._start = start } } + _callObserver (transaction, parentSubs, remote) { + this._callEventHandler(transaction, new YTextEvent(this, remote, transaction)) + } toString () { let str = '' let n = this._start @@ -82,10 +402,27 @@ export default class YText extends YArray { } return str } + applyDelta (delta) { + this._transact(y => { + let left = null + let right = this._start + const currentAttributes = new Map() + for (let i = 0; i < delta.length; i++) { + let op = delta[i] + if (op.hasOwnProperty('insert')) { + ;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {}) + } else if (op.hasOwnProperty('retain')) { + ;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {}) + } else if (op.hasOwnProperty('delete')) { + ;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes) + } + } + }) + } /** * As defined by Quilljs - https://quilljs.com/docs/delta/ */ - toRichtextDelta () { + toDelta () { let ops = [] let currentAttributes = new Map() let str = '' @@ -94,10 +431,16 @@ export default class YText extends YArray { if (str.length > 0) { // pack str with attributes to ops let attributes = {} + let addAttributes = false for (let [key, value] of currentAttributes) { + addAttributes = true attributes[key] = value } - ops.push({ insert: str, attributes }) + let op = { insert: str } + if (addAttributes) { + op.attributes = attributes + } + ops.push(op) str = '' } } @@ -109,13 +452,7 @@ export default class YText extends YArray { break case ItemFormat: packStr() - const value = n.value - const key = n.key - if (value === null) { - currentAttributes.delete(key) - } else { - currentAttributes.set(key, value) - } + updateCurrentAttributes(currentAttributes, n) break } } @@ -129,72 +466,35 @@ export default class YText extends YArray { return } this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos, attributes) - let negatedAttributes = new Map() - // insert format-start items - for (let key in attributes) { - const val = attributes[key] - const currentVal = currentAttributes.get(key) - if (currentVal !== val) { - // save negated attribute (set null if currentVal undefined) - negatedAttributes.set(key, currentVal || null) - let format = new ItemFormat() - format.key = key - format.value = val - integrateItem(format, this, y, left, right) - left = format - } - } - // insert text content - let item = new ItemString() - item._content = text - integrateItem(item, this, y, left, right) - left = item - // negate applied formats - for (let [key, value] of negatedAttributes) { - let format = new ItemFormat() - format.key = key - format.value = value - integrateItem(format, this, y, left, right) - left = format - } + let [left, right, currentAttributes] = findPosition(this, pos) + insertText(y, text, this, left, right, currentAttributes, attributes) + }) + } + insertEmbed (pos, embed, attributes = {}) { + if (embed.constructor !== Object) { + throw new Error('Embed must be an Object') + } + this._transact(y => { + let [left, right, currentAttributes] = findPosition(this, pos) + insertText(y, embed, this, left, right, currentAttributes, attributes) + }) + } + delete (pos, length) { + if (length === 0) { + return + } + this._transact(y => { + let [left, right, currentAttributes] = findPosition(this, pos) + deleteText(y, length, this, left, right, currentAttributes) }) } format (pos, length, attributes) { this._transact(y => { - let [left, _right, currentAttributes] = findPosition(this, pos, attributes) - if (_right === null) { + let [left, right, currentAttributes] = findPosition(this, pos) + if (right === null) { return } - let negatedAttributes = new Map() - // insert format-start items - for (let key in attributes) { - const val = attributes[key] - const currentVal = currentAttributes.get(key) - if (currentVal !== val) { - // save negated attribute (set null if currentVal undefined) - negatedAttributes.set(key, currentVal || null) - let format = new ItemFormat() - format.key = key - format.value = val - integrateItem(format, this, y, left, _right) - left = format - } - } - // iterate until first non-format or null is found - // delete all formats with attributes[format.key] != null - while (length > 0 && left !== null) { - if (left._deleted === false) { - if (left.constructor === ItemFormat) { - if (attributes[left.key] != null) { - left.delete(y) - } - } else if (length < left._length) { - - } - } - left = left._right - } + formatText(y, length, this, left, right, currentAttributes, attributes) }) } _logString () { diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index 53d82f48..9a09c7a0 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -6,6 +6,8 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Type/y-xml/y-x import Delete from '../Struct/Delete.js' import ItemJSON from '../Struct/ItemJSON.js' import ItemString from '../Struct/ItemString.js' +import ItemFormat from '../Struct/ItemFormat.js' +import ItemEmbed from '../Struct/ItemEmbed.js' const structs = new Map() const references = new Map() @@ -23,8 +25,11 @@ export function getReference (typeConstructor) { return references.get(typeConstructor) } +// TODO: reorder (Item* should have low numbers) addStruct(0, ItemJSON) addStruct(1, ItemString) +addStruct(10, ItemFormat) +addStruct(11, ItemEmbed) addStruct(2, Delete) addStruct(3, YArray) diff --git a/src/Y.js b/src/Y.js index 568e36ff..fd29a6e9 100644 --- a/src/Y.js +++ b/src/Y.js @@ -23,6 +23,7 @@ import debug from 'debug' import Transaction from './Transaction.js' import TextareaBinding from './Binding/TextareaBinding.js' +import QuillBinding from './Binding/QuillBinding.js' import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' @@ -202,6 +203,7 @@ Y.XmlText = YXmlText Y.XmlHook = YXmlHook Y.TextareaBinding = TextareaBinding +Y.QuillBinding = QuillBinding Y.utils = { BinaryDecoder, diff --git a/test/y-text.tests.js b/test/y-text.tests.js new file mode 100644 index 00000000..4b73ec11 --- /dev/null +++ b/test/y-text.tests.js @@ -0,0 +1,102 @@ +import { initArrays, compareUsers, flushAll } from '../tests-lib/helper.js' +import { test, proxyConsole } from 'cutest' + +proxyConsole() + +test('basic insert delete', async function text0 (t) { + let { users, text0 } = await initArrays(t, { users: 2 }) + let delta + + text0.observe(function (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 }]) + + await compareUsers(t, users) +}) + +test('basic format', async function text1 (t) { + let { users, text0 } = await initArrays(t, { users: 2 }) + let delta + text0.observe(function (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 } }]) + t.assert(text0._start._right._right._right._content === '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 } }]) + await compareUsers(t, users) +}) + +test('quill issue 1', async function quill1 (t) { + let { users, quill0 } = await initArrays(t, { users: 2 }) + quill0.insertText(0, 'x') + await flushAll(t, users) + quill0.insertText(1, '\n', 'list', 'ordered') + await flushAll(t, users) + quill0.insertText(1, '\n', 'list', 'ordered') + await compareUsers(t, users) +}) + +test('quill issue 2', async function quill2 (t) { + let { users, quill0, text0 } = await initArrays(t, { users: 2 }) + let delta + text0.observe(function (event) { + delta = event.delta + }) + quill0.insertText(0, 'abc', 'bold', true) + await flushAll(t, users) + quill0.insertText(1, 'x') + quill0.update() + t.compare(delta, [{ retain: 1 }, { insert: 'x', attributes: { bold: true } }]) + await compareUsers(t, users) +}) + +test('quill issue 3', async function quill3 (t) { + let { users, quill0, text0 } = await initArrays(t, { users: 2 }) + quill0.insertText(0, 'a') + quill0.insertText(1, '\n\n', 'list', 'ordered') + quill0.insertText(2, 'b') + t.compare(text0.toDelta(), [ + { insert: 'a' }, + { insert: '\n', attributes: { list: 'ordered' } }, + { insert: 'b' }, + { insert: '\n', attributes: { list: 'ordered' } } + ]) + await compareUsers(t, users) +}) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index ebacea27..a760ab19 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -6,6 +6,7 @@ import Chance from 'chance' import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' +import Quill from 'quill' export const Y = _Y @@ -92,9 +93,14 @@ export async function compareUsers (t, users) { await wait() await flushAll(t, users) - var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val))) - var userMapValues = users.map(u => u.get('map', Y.Map).toJSON()) - var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString()) + var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val))) + var userMapValues = users.map(u => u.define('map', Y.Map).toJSON()) + var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString()) + var userTextValues = users.map(u => u.define('text', Y.Text).toDelta()) + var userQuillValues = users.map(u => { + u.quill.update('yjs') // get latest changes + return u.quill.getContents().ops + }) var data = users.map(u => { defragmentItemContent(u) @@ -124,6 +130,8 @@ export async function compareUsers (t, users) { t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types') t.compare(userMapValues[i], userMapValues[i + 1], 'map types') t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types') + t.compare(userTextValues[i], userTextValues[i + 1], 'text types') + t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content') t.compare(data[i].os, data[i + 1].os, 'os') t.compare(data[i].ds, data[i + 1].ds, 'ds') t.compare(data[i].ss, data[i + 1].ss, 'ss') @@ -153,6 +161,13 @@ export async function initArrays (t, opts) { result['array' + i] = y.define('array', Y.Array) result['map' + i] = y.define('map', Y.Map) result['xml' + i] = y.define('xml', Y.XmlElement) + const textType = y.define('text', Y.Text) + result['text' + i] = textType + const quill = new Quill(document.createElement('div')) + const quillBinding = new Y.QuillBinding(textType, quill) + result['quill' + i] = quill + result['quillBinding' + i] = quillBinding + y.quill = quill // put quill on the y object (so we can use it later) y.get('xml').setDomFilter(function (nodeName, attrs) { if (nodeName === 'HIDDEN') { return null From f35c056bde056c92161054635b47ea7c0a734e43 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 26 Feb 2018 03:23:22 +0100 Subject: [PATCH 03/22] fix some tests --- rollup.test.js | 2 +- src/Type/YArray.js | 44 +++++++++++++++++++++++--------------------- test/index.js | 1 + 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/rollup.test.js b/rollup.test.js index 7e948a42..f4072276 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' import multiEntry from 'rollup-plugin-multi-entry' export default { - input: 'test/y-text.tests.js', + input: 'test/index.js', name: 'y-tests', sourcemap: true, output: { diff --git a/src/Type/YArray.js b/src/Type/YArray.js index 90835566..b017266b 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -225,29 +225,31 @@ export default class YArray extends Type { }) } insert (pos, content) { - let left = null - let right = this._start - let count = 0 - const y = this._y - while (right !== null) { - const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= pos && pos <= count + rightLen) { - const splitDiff = pos - count - right = right._splitAt(y, splitDiff) - left = right._left - count += splitDiff - break + this._transact(() => { + let left = null + let right = this._start + let count = 0 + const y = this._y + while (right !== null) { + const rightLen = right._deleted ? 0 : (right._length - 1) + if (count <= pos && pos <= count + rightLen) { + const splitDiff = pos - count + right = right._splitAt(y, splitDiff) + left = right._left + count += splitDiff + break + } + if (!right._deleted) { + count += right._length + } + left = right + right = right._right } - if (!right._deleted) { - count += right._length + if (pos > count) { + throw new Error('Position exceeds array range!') } - left = right - right = right._right - } - if (pos > count) { - throw new Error('Position exceeds array range!') - } - this.insertAfter(left, content) + this.insertAfter(left, content) + }) } push (content) { let n = this._start diff --git a/test/index.js b/test/index.js index f59d2a8b..1ef3afc8 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ import './red-black-tree.js' import './y-array.tests.js' +import './y-text.tests.js' import './y-map.tests.js' import './y-xml.tests.js' import './encode-decode.tests.js' From 384a4b72b09fec745da79b6a2872776161e2c808 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 26 Feb 2018 17:15:27 +0100 Subject: [PATCH 04/22] add quill-cursors example --- examples/quill-cursors/index.html | 21 +++++++++ examples/quill-cursors/index.js | 78 +++++++++++++++++++++++++++++++ examples/quill/index.html | 6 +-- examples/quill/index.js | 2 +- package-lock.json | 40 ++++++++++++---- package.json | 1 + 6 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 examples/quill-cursors/index.html create mode 100644 examples/quill-cursors/index.js diff --git a/examples/quill-cursors/index.html b/examples/quill-cursors/index.html new file mode 100644 index 00000000..1e63961c --- /dev/null +++ b/examples/quill-cursors/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + +
+
+
+
+ + + diff --git a/examples/quill-cursors/index.js b/examples/quill-cursors/index.js new file mode 100644 index 00000000..3b14ac9f --- /dev/null +++ b/examples/quill-cursors/index.js @@ -0,0 +1,78 @@ +/* global Y, Quill, QuillCursors */ + +Quill.register('modules/cursors', QuillCursors) + +let y = new Y('quill-0', { + connector: { + name: 'websockets-client', + url: 'http://127.0.0.1:1234' + } +}) +let users = y.define('users', Y.Array) +let myUserInfo = new Y.Map() +myUserInfo.set('name', 'dada') +myUserInfo.set('color', 'red') +users.push([myUserInfo]) + +let quill = new Quill('#quill-container', { + modules: { + toolbar: [ + [{ header: [1, 2, false] }], + ['bold', 'italic', 'underline'], + ['image', 'code-block'], + [{ color: [] }, { background: [] }], // Snow theme fills in values + [{ script: 'sub' }, { script: 'super' }], + ['link', 'image'], + ['link', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }] + ], + cursors: { + hideDelay: 500 + } + }, + placeholder: 'Compose an epic...', + theme: 'snow' // or 'bubble' +}) + +let cursors = quill.getModule('cursors') + +function drawCursors () { + cursors.clearCursors() + users.map((user, userId) => { + if (user !== myUserInfo) { + let relativeRange = user.get('range') + let lastUpdated = new Date(user.get('last updated')) + if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) { + let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset + let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset + let range = { index: start, length: end - start } + cursors.setCursor(userId + '', range, user.get('name'), user.get('color')) + } + } + }) +} + +users.observeDeep(drawCursors) +drawCursors() + +quill.on('selection-change', function (range) { + if (range != null) { + myUserInfo.set('range', { + start: Y.utils.getRelativePosition(yText, range.index), + end: Y.utils.getRelativePosition(yText, range.index + range.length) + }) + } else { + myUserInfo.delete('range') + } + myUserInfo.set('last updated', new Date().toString()) +}) + +let yText = y.define('quill', Y.Text) +let quillBinding = new Y.QuillBinding(yText, quill) + +window.quillBinding = quillBinding +window.yText = yText +window.y = y +window.quill = quill +window.users = users +window.cursors = cursors diff --git a/examples/quill/index.html b/examples/quill/index.html index 0f881d27..27a1964b 100644 --- a/examples/quill/index.html +++ b/examples/quill/index.html @@ -2,9 +2,9 @@ - - - + + + diff --git a/examples/quill/index.js b/examples/quill/index.js index 8e2ab6e9..87ec0de9 100644 --- a/examples/quill/index.js +++ b/examples/quill/index.js @@ -1,6 +1,6 @@ /* global Y, Quill */ -let y = new Y('htmleditor10', { +let y = new Y('quill-cursors-0', { connector: { name: 'websockets-client', url: 'http://127.0.0.1:1234' diff --git a/package-lock.json b/package-lock.json index 112a1bcb..c8facecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3337,6 +3337,16 @@ "quill-delta": "3.6.2" } }, + "quill-cursors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/quill-cursors/-/quill-cursors-1.0.2.tgz", + "integrity": "sha512-mWkhOA9TvdFklG1QwVAOS70hOSpiHiJ+eoIbSeEXI6no6wNQLavYo3eWYHXgvi6Z5/SjS0oSn+NLdpYuXgdA8Q==", + "dev": true, + "requires": { + "rangefix": "0.2.5", + "tinycolor2": "1.4.1" + } + }, "quill-delta": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.2.tgz", @@ -3395,6 +3405,12 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", "dev": true }, + "rangefix": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rangefix/-/rangefix-0.2.5.tgz", + "integrity": "sha1-vOeMkhsjWCuuIR9ZdGSlkf0alPk=", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4033,6 +4049,15 @@ "duplexer": "0.1.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -4044,15 +4069,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -4168,6 +4184,12 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "dev": true + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", diff --git a/package.json b/package.json index 1290580a..b53df6ef 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "concurrently": "^3.4.0", "cutest": "^0.1.9", "quill": "^1.3.5", + "quill-cursors": "^1.0.2", "rollup-plugin-babel": "^2.7.1", "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-inject": "^2.0.0", From dc22a79ac4f914794c628d0cfa0bd2b6e65bf21a Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 27 Feb 2018 03:52:40 +0100 Subject: [PATCH 05/22] properly unregister event when binding is destroyed --- src/Binding/QuillBinding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Binding/QuillBinding.js b/src/Binding/QuillBinding.js index 8920f20f..74bb21bc 100644 --- a/src/Binding/QuillBinding.js +++ b/src/Binding/QuillBinding.js @@ -31,7 +31,7 @@ export default class QuillBinding extends Binding { destroy () { // Remove everything that is handled by this class this.type.unobserve(this._typeObserver) - this.target.unobserve(this._quillObserver) + this.target.off('text-change', this._quillObserver) super.destroy() } } From a9b610479df8df65338a068afa827d2cc2b1490c Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 5 Mar 2018 03:03:40 +0100 Subject: [PATCH 06/22] big documentation update - all public functions and classes are documented now --- .esdoc.json | 10 + .gitignore | 1 + documentation.yml | 43 + package-lock.json | 1168 ++++++++++++++++-- package.json | 8 +- src/Binary/Decoder.js | 37 +- src/Binary/Encoder.js | 63 +- src/Binding/Binding.js | 22 + src/Binding/QuillBinding.js | 23 +- src/Binding/TextareaBinding.js | 11 + src/MessageHandler/binaryEncode.js | 14 + src/MessageHandler/integrateRemoteStructs.js | 1 + src/MessageHandler/syncStep1.js | 5 + src/Persistence.js | 4 + src/Struct/Delete.js | 43 +- src/Struct/Item.js | 101 +- src/Struct/Type.js | 75 ++ src/Transaction.js | 8 + src/Type/YArray.js | 140 ++- src/Type/YMap.js | 66 +- src/Type/YText.js | 130 +- src/Type/y-xml/YXmlElement.js | 94 +- src/Type/y-xml/YXmlEvent.js | 3 + src/Type/y-xml/YXmlFragment.js | 158 ++- src/Type/y-xml/YXmlHook.js | 50 + src/Type/y-xml/YXmlText.js | 33 + src/Type/y-xml/utils.js | 4 +- src/Util/EventHandler.js | 34 + src/Util/NamedEventHandler.js | 50 + src/Util/UndoManager.js | 16 + src/Util/YEvent.js | 18 + src/Util/relativePosition.js | 50 + src/Y.js | 111 +- 33 files changed, 2426 insertions(+), 168 deletions(-) create mode 100644 .esdoc.json create mode 100644 documentation.yml diff --git a/.esdoc.json b/.esdoc.json new file mode 100644 index 00000000..90752511 --- /dev/null +++ b/.esdoc.json @@ -0,0 +1,10 @@ +{ + "source": "./src", + "destination": "./docs", + "plugins": [{ + "name": "esdoc-standard-plugin", + "option": { + "accessor": {"access": ["public"], "autoPrivate": true} + } + }] +} diff --git a/.gitignore b/.gitignore index 1173b99b..d784cd40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules bower_components +docs /y.* /examples/yjs-dist.js* diff --git a/documentation.yml b/documentation.yml new file mode 100644 index 00000000..925badf2 --- /dev/null +++ b/documentation.yml @@ -0,0 +1,43 @@ +toc: + - Y + - name: Shared Types + description: | + Types provide an abstraction over the shared data. + Shared types can be edited concurrently by peers. + - Type + - YArray + - YMap + - YText + - YXmlElement + - YXmlFragment + - YXmlHook + - YXmlText + - name: Bindings + description: | + A binding handles data binding from a Yjs type to + a data object + - Binding + - DomBinding + - QuillBinding + - TextareaBinding + - name: Events + description: | + Events describe changes on shared types. + - YArrayEvent + - YEvent + - YMapEvent + - YTextEvent + - YXmlEvent + - name: Binary Encoding + description: | + Yjs efficiently encodes the Yjs model to a binary format. + This section describes utility functions for binary encoding and decoding. + - BinaryEncoder + - BinaryDecoder + - toBinary + - fromBinary + - name: Relative Position + - RelativePosition + - fromRelativePosition + - getRelativePosition + - name: Utility diff --git a/package-lock.json b/package-lock.json index c8facecd..cdd94b4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "optional": true + }, "accepts": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", @@ -20,6 +26,23 @@ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", "dev": true }, + "acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "optional": true, + "requires": { + "acorn": "2.7.0" + }, + "dependencies": { + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", + "optional": true + } + } + }, "acorn-jsx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", @@ -73,14 +96,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "anymatch": { "version": "1.3.0", @@ -180,6 +201,17 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "optional": true + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "async-array-reduce": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/async-array-reduce/-/async-array-reduce-0.2.1.tgz", @@ -192,6 +224,24 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "optional": true + }, "babel-cli": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.24.1.tgz", @@ -410,7 +460,6 @@ "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, "requires": { "babel-runtime": "6.23.0" } @@ -827,7 +876,6 @@ "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", - "dev": true, "requires": { "core-js": "2.4.1", "regenerator-runtime": "0.10.5" @@ -867,7 +915,6 @@ "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", - "dev": true, "requires": { "babel-runtime": "6.23.0", "esutils": "2.0.2", @@ -899,6 +946,15 @@ "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -911,6 +967,20 @@ "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", "dev": true }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "optional": true, + "requires": { + "hoek": "4.2.1" + } + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -994,6 +1064,12 @@ } } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -1016,7 +1092,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, "requires": { "ansi-styles": "2.2.1", "escape-string-regexp": "1.0.5", @@ -1031,6 +1106,29 @@ "integrity": "sha1-A1ALBK2U53jdKJGwnsc6ath7GZY=", "dev": true }, + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "requires": { + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.9.2", + "lodash.assignin": "4.2.0", + "lodash.bind": "4.2.1", + "lodash.defaults": "4.2.0", + "lodash.filter": "4.6.0", + "lodash.flatten": "4.4.0", + "lodash.foreach": "4.5.0", + "lodash.map": "4.6.0", + "lodash.merge": "4.6.1", + "lodash.pick": "4.4.0", + "lodash.reduce": "4.6.0", + "lodash.reject": "4.6.0", + "lodash.some": "4.6.0" + } + }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -1088,8 +1186,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, "code-point-at": { "version": "1.1.0", @@ -1097,6 +1194,19 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "color-logger": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/color-logger/-/color-logger-0.0.3.tgz", + "integrity": "sha1-2bIt0dlz4Waxi/MT+fSBu6TfIBg=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, "commander": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz", @@ -1251,14 +1361,62 @@ "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", - "dev": true + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "optional": true, + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "optional": true, + "requires": { + "hoek": "4.2.1" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=" + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "optional": true, + "requires": { + "cssom": "0.3.2" + } }, "currently-unhandled": { "version": "0.4.1", @@ -1289,6 +1447,15 @@ "es5-ext": "0.10.23" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "requires": { + "assert-plus": "1.0.0" + } + }, "date-fns": { "version": "1.28.5", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.28.5.tgz", @@ -1324,8 +1491,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "define-properties": { "version": "1.1.2", @@ -1366,6 +1532,11 @@ "rimraf": "2.6.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "depd": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", @@ -1382,7 +1553,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, "requires": { "repeating": "2.0.1" } @@ -1397,12 +1567,59 @@ "isarray": "1.0.0" } }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1415,6 +1632,11 @@ "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=", "dev": true }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -1529,14 +1751,33 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", + "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "optional": true, + "requires": { + "esprima": "3.1.3", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } }, "escope": { "version": "3.6.0", @@ -1550,6 +1791,233 @@ "estraverse": "4.2.0" } }, + "esdoc": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esdoc/-/esdoc-1.0.4.tgz", + "integrity": "sha512-Hy5sg0Lec4EDHVem3gFqNi+o6ZptivmaiHYacZhmn3hzLnHSMg2C1L0XTsDIcb4Cxd9aUnWdLAu6a6ghH/LLug==", + "requires": { + "babel-generator": "6.26.0", + "babel-traverse": "6.26.0", + "babylon": "6.18.0", + "cheerio": "0.22.0", + "color-logger": "0.0.3", + "escape-html": "1.0.3", + "fs-extra": "1.0.0", + "ice-cap": "0.0.4", + "marked": "0.3.6", + "minimist": "1.2.0", + "taffydb": "2.7.2" + }, + "dependencies": { + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", + "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.6", + "trim-right": "1.0.1" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.11.1" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "esdoc-accessor-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-accessor-plugin/-/esdoc-accessor-plugin-1.0.0.tgz", + "integrity": "sha1-eRukhy5sQDUVznSbE0jW8Ck62es=" + }, + "esdoc-brand-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-brand-plugin/-/esdoc-brand-plugin-1.0.0.tgz", + "integrity": "sha1-niFtc15i/OxJ96M5u0Eh2mfMYDM=", + "requires": { + "cheerio": "0.22.0" + } + }, + "esdoc-coverage-plugin": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esdoc-coverage-plugin/-/esdoc-coverage-plugin-1.1.0.tgz", + "integrity": "sha1-OGmGnNf4eJH5cmJXh2laKZrs5Fw=" + }, + "esdoc-external-ecmascript-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-external-ecmascript-plugin/-/esdoc-external-ecmascript-plugin-1.0.0.tgz", + "integrity": "sha1-ePVl1KDFGFrGMVJhTc4f4ahmiNs=", + "requires": { + "fs-extra": "1.0.0" + } + }, + "esdoc-integrate-manual-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-integrate-manual-plugin/-/esdoc-integrate-manual-plugin-1.0.0.tgz", + "integrity": "sha1-GFSmqhwIEDXXyMUeO91PtlqkcRw=" + }, + "esdoc-integrate-test-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-integrate-test-plugin/-/esdoc-integrate-test-plugin-1.0.0.tgz", + "integrity": "sha1-4tDQAJD38MNeXS8sAzMnp55T5Ak=" + }, + "esdoc-lint-plugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esdoc-lint-plugin/-/esdoc-lint-plugin-1.0.1.tgz", + "integrity": "sha1-h77mQD5nbAh/Yb6SxFLWDyxqcOU=" + }, + "esdoc-publish-html-plugin": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esdoc-publish-html-plugin/-/esdoc-publish-html-plugin-1.1.0.tgz", + "integrity": "sha1-CT+DN6yhaQIlcss4f/zD9HCwJRM=", + "requires": { + "babel-generator": "6.11.4", + "cheerio": "0.22.0", + "escape-html": "1.0.3", + "fs-extra": "1.0.0", + "ice-cap": "0.0.4", + "marked": "0.3.6", + "taffydb": "2.7.2" + }, + "dependencies": { + "babel-generator": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.11.4.tgz", + "integrity": "sha1-FPaTOrsgxiZm0n47e59bncBxKpo=", + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "detect-indent": "3.0.1", + "lodash": "4.17.4", + "source-map": "0.5.6" + } + }, + "detect-indent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-3.0.1.tgz", + "integrity": "sha1-ncXl3bzu+DJXZLlFGwK8bVQIT3U=", + "requires": { + "get-stdin": "4.0.1", + "minimist": "1.2.0", + "repeating": "1.1.3" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "repeating": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", + "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "requires": { + "is-finite": "1.0.2" + } + } + } + }, + "esdoc-standard-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-standard-plugin/-/esdoc-standard-plugin-1.0.0.tgz", + "integrity": "sha1-ZhIBysfvhokkkCRG/awVJyU8XU0=", + "requires": { + "esdoc-accessor-plugin": "1.0.0", + "esdoc-brand-plugin": "1.0.0", + "esdoc-coverage-plugin": "1.1.0", + "esdoc-external-ecmascript-plugin": "1.0.0", + "esdoc-integrate-manual-plugin": "1.0.0", + "esdoc-integrate-test-plugin": "1.0.0", + "esdoc-lint-plugin": "1.0.1", + "esdoc-publish-html-plugin": "1.1.0", + "esdoc-type-inference-plugin": "1.0.1", + "esdoc-undocumented-identifier-plugin": "1.0.0", + "esdoc-unexported-identifier-plugin": "1.0.0" + } + }, + "esdoc-type-inference-plugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esdoc-type-inference-plugin/-/esdoc-type-inference-plugin-1.0.1.tgz", + "integrity": "sha1-qrynhkH5m9Hs5vMC8EW71jG+cvU=" + }, + "esdoc-undocumented-identifier-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-undocumented-identifier-plugin/-/esdoc-undocumented-identifier-plugin-1.0.0.tgz", + "integrity": "sha1-guBdNxwy0ShxFA8dXIHsmf2cwsg=" + }, + "esdoc-unexported-identifier-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esdoc-unexported-identifier-plugin/-/esdoc-unexported-identifier-plugin-1.0.0.tgz", + "integrity": "sha1-H5h0xqfCvr+a05fDzrdcnGnaurE=" + }, "eslint": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", @@ -1738,8 +2206,7 @@ "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" }, "esquery": { "version": "1.0.0", @@ -1763,8 +2230,7 @@ "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" }, "estree-walker": { "version": "0.2.1", @@ -1775,8 +2241,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "event-emitter": { "version": "0.3.5", @@ -1830,8 +2295,7 @@ "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" }, "extend-shallow": { "version": "2.0.1", @@ -1851,23 +2315,39 @@ "is-extglob": "1.0.0" } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "ez-async": { "version": "1.0.0-alpha.1", "resolved": "https://registry.npmjs.org/ez-async/-/ez-async-1.0.0-alpha.1.tgz", "integrity": "sha1-ysNCuPqJAm7+c6Jg/p9rgE9J5H8=", "dev": true }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "optional": true + }, "fast-diff": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", "dev": true }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "optional": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "faye-websocket": { "version": "0.11.1", @@ -1996,6 +2476,23 @@ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", "dev": true }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.15" + } + }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -2008,6 +2505,16 @@ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", "dev": true }, + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "klaw": "1.3.1" + } + }, "fs-readdir-recursive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz", @@ -2047,6 +2554,15 @@ "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "requires": { + "assert-plus": "1.0.0" + } + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -2105,8 +2621,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" }, "globby": { "version": "5.0.0", @@ -2125,8 +2640,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "graceful-readlink": { "version": "1.0.1", @@ -2134,6 +2648,36 @@ "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", "dev": true }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "optional": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "optional": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + } + } + }, "has": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", @@ -2147,7 +2691,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -2167,6 +2710,23 @@ "is-glob": "2.0.1" } }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "optional": true, + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -2192,6 +2752,19 @@ "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", "dev": true }, + "htmlparser2": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.1", + "domutils": "1.5.1", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, "http-auth": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", @@ -2216,6 +2789,89 @@ "statuses": "1.3.1" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "ice-cap": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ice-cap/-/ice-cap-0.0.4.tgz", + "integrity": "sha1-im0xq0ysjUtW3k+pRt8zUlYbbhg=", + "requires": { + "cheerio": "0.20.0", + "color-logger": "0.0.3" + }, + "dependencies": { + "cheerio": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.20.0.tgz", + "integrity": "sha1-XHEPK6uVZTJyhCugHG6mGzVF7DU=", + "requires": { + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.8.3", + "jsdom": "7.2.2", + "lodash": "4.17.4" + } + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.3.0", + "domutils": "1.5.1", + "entities": "1.0.0", + "readable-stream": "1.1.14" + }, + "dependencies": { + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + } + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "ignore": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", @@ -2250,8 +2906,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.4", @@ -2290,7 +2945,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", - "dev": true, "requires": { "loose-envify": "1.3.1" } @@ -2368,7 +3022,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, "requires": { "number-is-nan": "1.0.1" } @@ -2484,6 +3137,12 @@ "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", "dev": true }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -2511,8 +3170,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2529,11 +3187,16 @@ "isarray": "1.0.0" } }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "js-yaml": { "version": "3.8.4", @@ -2545,11 +3208,59 @@ "esprima": "3.1.3" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", + "integrity": "sha1-QLQCdwwr2iNGkJa+6Rq2deOx/G4=", + "optional": true, + "requires": { + "abab": "1.0.4", + "acorn": "2.7.0", + "acorn-globals": "1.0.9", + "cssom": "0.3.2", + "cssstyle": "0.2.37", + "escodegen": "1.9.1", + "nwmatcher": "1.4.3", + "parse5": "1.5.1", + "request": "2.83.0", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.4", + "webidl-conversions": "2.0.1", + "whatwg-url-compat": "0.6.5", + "xml-name-validator": "2.0.1" + }, + "dependencies": { + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", + "optional": true + } + } + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "optional": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "optional": true }, "json-stable-stringify": { "version": "1.0.1", @@ -2560,12 +3271,26 @@ "jsonify": "0.0.0" } }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "4.1.11" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -2578,6 +3303,18 @@ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "dev": true }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "jsx-ast-utils": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", @@ -2593,6 +3330,14 @@ "is-buffer": "1.1.5" } }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "requires": { + "graceful-fs": "4.1.11" + } + }, "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -2606,7 +3351,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "1.1.2", "type-check": "0.3.2" @@ -2786,8 +3530,17 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" }, "lodash.cond": { "version": "4.5.2", @@ -2795,6 +3548,56 @@ "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -2805,7 +3608,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, "requires": { "js-tokens": "3.0.2" } @@ -2841,6 +3643,11 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" + }, "matched": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/matched/-/matched-0.4.4.tgz", @@ -2908,14 +3715,12 @@ "mime-db": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", - "dev": true + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" }, "mime-types": { "version": "2.1.15", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", - "dev": true, "requires": { "mime-db": "1.27.0" } @@ -3001,11 +3806,30 @@ "remove-trailing-separator": "1.0.2" } }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "requires": { + "boolbase": "1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nwmatcher": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", + "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==", + "optional": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3074,7 +3898,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, "requires": { "deep-is": "0.1.3", "fast-levenshtein": "2.0.6", @@ -3087,8 +3910,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" } } }, @@ -3163,6 +3985,12 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", + "optional": true + }, "parseurl": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", @@ -3216,6 +4044,12 @@ "through": "2.3.8" } }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -3296,8 +4130,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "preserve": { "version": "0.2.0", @@ -3314,8 +4147,7 @@ "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "progress": { "version": "1.1.8", @@ -3323,6 +4155,17 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "optional": true + }, "quill": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.5.tgz", @@ -3460,7 +4303,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true, "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -3522,8 +4364,7 @@ "regenerator-runtime": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" }, "regenerator-transform": { "version": "0.9.11", @@ -3602,11 +4443,57 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, "requires": { "is-finite": "1.0.2" } }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "optional": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + }, + "dependencies": { + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "optional": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "optional": true, + "requires": { + "mime-db": "1.33.0" + } + } + } + }, "require-relative": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", @@ -3830,8 +4717,13 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "optional": true }, "semver": { "version": "5.3.0", @@ -3904,11 +4796,19 @@ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", "dev": true }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "optional": true, + "requires": { + "hoek": "4.2.1" + } + }, "source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", - "dev": true + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" }, "source-map-support": { "version": "0.4.15", @@ -3961,6 +4861,22 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, "stack-generator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.1.tgz", @@ -4049,15 +4965,6 @@ "duplexer": "0.1.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -4069,11 +4976,24 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "optional": true + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -4110,8 +5030,13 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "optional": true }, "table": { "version": "3.8.3", @@ -4160,6 +5085,11 @@ } } }, + "taffydb": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.7.2.tgz", + "integrity": "sha1-e/gQalwaSCUbPjvAoOFzJIn9Dcg=" + }, "tag-dist-files": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/tag-dist-files/-/tag-dist-files-0.1.6.tgz", @@ -4193,8 +5123,7 @@ "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, "to-object-path": { "version": "0.3.0", @@ -4205,6 +5134,20 @@ "kind-of": "3.2.2" } }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "optional": true + }, "tree-kill": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.1.0.tgz", @@ -4220,8 +5163,7 @@ "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, "tryit": { "version": "1.0.3", @@ -4229,11 +5171,25 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "1.1.2" } @@ -4289,8 +5245,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.0", @@ -4301,8 +5256,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", - "dev": true + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "v8flags": { "version": "2.1.1", @@ -4329,12 +5283,29 @@ "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=", "dev": true }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, "vlq": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz", "integrity": "sha1-4xbVJXtAuGu0PLjV/qXX9U1rDKE=", "dev": true }, + "webidl-conversions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz", + "integrity": "sha1-O/glj30xjHRDw28uFpQCoaZwNQY=", + "optional": true + }, "websocket-driver": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", @@ -4350,6 +5321,15 @@ "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=", "dev": true }, + "whatwg-url-compat": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz", + "integrity": "sha1-AImBEa9om7CXVBzVpFymyHmERb8=", + "optional": true, + "requires": { + "tr46": "0.0.3" + } + }, "which": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", @@ -4386,6 +5366,12 @@ "mkdirp": "0.5.1" } }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", + "optional": true + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index b53df6ef..a936fb9d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "scripts": { "test": "npm run lint", "debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'", - "lint": "standard", + "lint": "standard && documentation lint src/**", + "docs": "documentation build src/** -f html -o docs", + "serve-docs": "documentation serve src/**", "dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js", "watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'", "postversion": "npm run dist", @@ -64,7 +66,9 @@ "rollup-regenerator-runtime": "^6.23.1", "rollup-watch": "^3.2.2", "standard": "^10.0.2", - "tag-dist-files": "^0.1.6" + "tag-dist-files": "^0.1.6", + "esdoc": "^1.0.4", + "esdoc-standard-plugin": "^1.0.0" }, "dependencies": { "debug": "^2.6.8" diff --git a/src/Binary/Decoder.js b/src/Binary/Decoder.js index fcc18b72..d872e037 100644 --- a/src/Binary/Decoder.js +++ b/src/Binary/Decoder.js @@ -1,7 +1,13 @@ import ID from '../Util/ID.js' import { default as RootID, RootFakeUserID } from '../Util/RootID.js' +/** + * A BinaryDecoder handles the decoding of an ArrayBuffer + */ export default class BinaryDecoder { + /** + * @param {Uint8Array|Buffer} buffer The binary data that this instance decodes + */ constructor (buffer) { if (buffer instanceof ArrayBuffer) { this.uint8arr = new Uint8Array(buffer) @@ -12,6 +18,7 @@ export default class BinaryDecoder { } this.pos = 0 } + /** * Clone this decoder instance * Optionally set a new position parameter @@ -21,26 +28,32 @@ export default class BinaryDecoder { decoder.pos = newPos return decoder } + /** * Number of bytes */ get length () { return this.uint8arr.length } + /** * Skip one byte, jump to the next position */ skip8 () { this.pos++ } + /** * Read one byte as unsigned integer */ readUint8 () { return this.uint8arr[this.pos++] } + /** * Read 4 bytes as unsigned integer + * + * @return number An unsigned integer */ readUint32 () { let uint = @@ -51,19 +64,24 @@ export default class BinaryDecoder { this.pos += 4 return uint } + /** * Look ahead without incrementing position * to the next byte and read it as unsigned integer + * + * @return number An unsigned integer */ peekUint8 () { return this.uint8arr[this.pos] } + /** * Read unsigned integer (32bit) with variable length * 1/8th of the storage is used as encoding overhead - * - numbers < 2^7 is stored in one byte - * - numbers < 2^14 is stored in two bytes - * .. + * * numbers < 2^7 is stored in one byte + * * numbers < 2^14 is stored in two bytes + * + * @return number An unsigned integer */ readVarUint () { let num = 0 @@ -80,9 +98,12 @@ export default class BinaryDecoder { } } } + /** * Read string of variable length - * - varUint is used to store the length of the string + * * varUint is used to store the length of the string + * + * @return string */ readVarString () { let len = this.readVarUint() @@ -94,7 +115,7 @@ export default class BinaryDecoder { return decodeURIComponent(escape(encodedString)) } /** - * Look ahead and read varString without incrementing position + * Look ahead and read varString without incrementing position */ peekVarString () { let pos = this.pos @@ -104,8 +125,10 @@ export default class BinaryDecoder { } /** * Read ID - * - If first varUint read is 0xFFFFFF a RootID is returned - * - Otherwise an ID is returned + * * If first varUint read is 0xFFFFFF a RootID is returned + * * Otherwise an ID is returned + * + * @return ID */ readID () { let user = this.readVarUint() diff --git a/src/Binary/Encoder.js b/src/Binary/Encoder.js index db53e8df..b31cb67d 100644 --- a/src/Binary/Encoder.js +++ b/src/Binary/Encoder.js @@ -3,41 +3,81 @@ import { RootFakeUserID } from '../Util/RootID.js' const bits7 = 0b1111111 const bits8 = 0b11111111 +/** + * A BinaryEncoder handles the encoding to an ArrayBuffer + */ export default class BinaryEncoder { constructor () { // TODO: implement chained Uint8Array buffers instead of Array buffer this.data = [] } + /** + * The current length of the encoded data + */ get length () { return this.data.length } + /** + * The current write pointer (the same as {@link length}). + */ get pos () { return this.data.length } + /** + * Create an ArrayBuffer + * + * @return {Uint8Array} + */ createBuffer () { return Uint8Array.from(this.data).buffer } + /** + * Write one byte as an unsigned integer + * + * @param {number} num The number that is to be encoded + */ writeUint8 (num) { this.data.push(num & bits8) } + /** + * Write one byte as an unsigned Integer at a specific location + * + * @param {number} pos The location where the data will be written + * @param {number} num The number that is to + */ setUint8 (pos, num) { this.data[pos] = num & bits8 } + /** + * Write two bytes as an unsigned integer + * + * @param {number} pos The number that is to be encoded + */ writeUint16 (num) { this.data.push(num & bits8, (num >>> 8) & bits8) } - + /** + * Write two bytes as an unsigned integer at a specific location + * + * @param {number} pos The location where the data will be written + * @param {number} num The number that is to + */ setUint16 (pos, num) { this.data[pos] = num & bits8 this.data[pos + 1] = (num >>> 8) & bits8 } + /** + * Write two bytes as an unsigned integer + * + * @param {number} pos The number that is to be encoded + */ writeUint32 (num) { for (let i = 0; i < 4; i++) { this.data.push(num & bits8) @@ -45,6 +85,12 @@ export default class BinaryEncoder { } } + /** + * Write two bytes as an unsigned integer at a specific location + * + * @param {number} pos The location where the data will be written + * @param {number} num The number that is to + */ setUint32 (pos, num) { for (let i = 0; i < 4; i++) { this.data[pos + i] = num & bits8 @@ -52,6 +98,11 @@ export default class BinaryEncoder { } } + /** + * Write a variable length unsigned integer + * + * @param {number} pos The number that is to be encoded + */ writeVarUint (num) { while (num >= 0b10000000) { this.data.push(0b10000000 | (bits7 & num)) @@ -60,6 +111,11 @@ export default class BinaryEncoder { this.data.push(bits7 & num) } + /** + * Write a variable length string. + * + * @param {number} pos The number that is to be encoded + */ writeVarString (str) { let encodedString = unescape(encodeURIComponent(str)) let bytes = encodedString.split('').map(c => c.codePointAt()) @@ -70,6 +126,11 @@ export default class BinaryEncoder { } } + /** + * Write an ID at the current position + * + * @param {ID} id + */ writeID (id) { const user = id.user this.writeVarUint(user) diff --git a/src/Binding/Binding.js b/src/Binding/Binding.js index a8998410..8ccab2c2 100644 --- a/src/Binding/Binding.js +++ b/src/Binding/Binding.js @@ -1,12 +1,34 @@ import { createMutualExclude } from '../Util/mutualExclude.js' +/** + * Abstract class for bindings + * + * A binding handles data binding from a Yjs type to a data object. For example, + * you can bind a Quill editor instance to a YText instance with the `QuillBinding` class. + * + * It is expected that a concrete implementation accepts two parameters + * (type and binding target). + * + * @example + * const quill = new Quill(document.createElement('div')) + * const type = y.define('quill', Y.Text) + * const binding = new Y.QuillBinding(quill, type) + * + */ export default class Binding { + /** + * @param {YType} type Yjs type + * @param {any} target Binding Target + */ constructor (type, target) { this.type = type this.target = target this._mutualExclude = createMutualExclude() } + /** + * Remove all data observers (both from the type and th target). + */ destroy () { this.type = null this.target = null diff --git a/src/Binding/QuillBinding.js b/src/Binding/QuillBinding.js index 74bb21bc..e88a6c44 100644 --- a/src/Binding/QuillBinding.js +++ b/src/Binding/QuillBinding.js @@ -1,4 +1,3 @@ - import Binding from './Binding.js' function typeObserver (event) { @@ -16,17 +15,29 @@ function quillObserver (delta) { }) } +/** + * A Binding that binds a YText type to a Quill editor + * + * @example + * const quill = new Quill(document.createElement('div')) + * const type = y.define('quill', Y.Text) + * const binding = new Y.QuillBinding(quill, type) + */ export default class QuillBinding extends Binding { - constructor (textType, quillInstance) { - // Binding handles textType as this.type and quillInstance as this.target - super(textType, quillInstance) + /** + * @param {YText} textType + * @param {Quill} quill + */ + constructor (textType, quill) { + // Binding handles textType as this.type and quill as this.target + super(textType, quill) // set initial value - quillInstance.setContents(textType.toDelta(), 'yjs') + quill.setContents(textType.toDelta(), 'yjs') // Observers are handled by this class this._typeObserver = typeObserver.bind(this) this._quillObserver = quillObserver.bind(this) textType.observe(this._typeObserver) - quillInstance.on('text-change', this._quillObserver) + quill.on('text-change', this._quillObserver) } destroy () { // Remove everything that is handled by this class diff --git a/src/Binding/TextareaBinding.js b/src/Binding/TextareaBinding.js index ab59f0f7..829bfe9d 100644 --- a/src/Binding/TextareaBinding.js +++ b/src/Binding/TextareaBinding.js @@ -24,6 +24,17 @@ function domObserver () { }) } +/** + * A binding that binds a YText to a dom textarea. + * + * This binding will automatically be destroyed when it's parent is deleted + * + * @example + * const textare = document.createElement('textarea') + * const type = y.define('textarea', Y.Text) + * const binding = new Y.QuillBinding(textarea, type) + * + */ export default class TextareaBinding extends Binding { constructor (textType, domTextarea) { // Binding handles textType as this.type and domTextarea as this.target diff --git a/src/MessageHandler/binaryEncode.js b/src/MessageHandler/binaryEncode.js index 3a767a13..dafdff36 100644 --- a/src/MessageHandler/binaryEncode.js +++ b/src/MessageHandler/binaryEncode.js @@ -1,8 +1,15 @@ + import { writeStructs } from './syncStep1.js' import { integrateRemoteStructs } from './integrateRemoteStructs.js' import { readDeleteSet, writeDeleteSet } from './deleteSet.js' import BinaryEncoder from '../Binary/Encoder.js' +/** + * Read the Decoder and fill the Yjs instance with data in the decoder. + * + * @param {Y} y The Yjs instance + * @param {BinaryDecoder} decoder The BinaryDecoder to read from. + */ export function fromBinary (y, decoder) { y.transact(function () { integrateRemoteStructs(y, decoder) @@ -10,6 +17,13 @@ export function fromBinary (y, decoder) { }) } +/** + * Encode the Yjs model to binary format. + * + * @param {Y} y The Yjs instance + * @return {BinaryEncoder} The encoder instance that can be transformed + * to ArrayBuffer or Buffer. + */ export function toBinary (y) { let encoder = new BinaryEncoder() writeStructs(y, encoder, new Map()) diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index 97b49211..4e02c752 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -11,6 +11,7 @@ class MissingEntry { } /** + * @private * Integrate remote struct * When a remote struct is integrated, other structs might be ready to ready to * integrate. diff --git a/src/MessageHandler/syncStep1.js b/src/MessageHandler/syncStep1.js index 468a7f88..ad73bf6c 100644 --- a/src/MessageHandler/syncStep1.js +++ b/src/MessageHandler/syncStep1.js @@ -30,6 +30,11 @@ export function sendSyncStep1 (connector, syncUser) { connector.send(syncUser, encoder.createBuffer()) } +/** + * @private + * Write all Items that are not not included in ss to + * the encoder object. + */ export function writeStructs (y, encoder, ss) { const lenPos = encoder.pos encoder.writeUint32(0) diff --git a/src/Persistence.js b/src/Persistence.js index 38766bb9..91bbdd2a 100644 --- a/src/Persistence.js +++ b/src/Persistence.js @@ -13,6 +13,10 @@ function getFreshCnf () { } } +/** + * @private + * Abstract persistence class. + */ export default class AbstractPersistence { constructor (opts) { this.opts = opts diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js index a42874b9..c6de21e7 100644 --- a/src/Struct/Delete.js +++ b/src/Struct/Delete.js @@ -3,6 +3,7 @@ import ID from '../Util/ID.js' import { logID } from '../MessageHandler/messageToString.js' /** + * @private * Delete all items in an ID-range * TODO: implement getItemCleanStartNode for better performance (only one lookup) */ @@ -35,13 +36,26 @@ export function deleteItemRange (y, user, clock, range) { } /** - * Delete is not a real struct. It will not be saved in OS + * @private + * A Delete change is not a real Item, but it provides the same interface as an + * Item. The only difference is that it will not be saved in the ItemStore + * (OperationStore), but instead it is safed in the DeleteStore. */ export default class Delete { constructor () { this._target = null this._length = null } + + /** + * @private + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {BinaryDecoder} decoder The decoder object to read data from. + */ _fromBinary (y, decoder) { // TODO: set target, and add it to missing if not found // There is an edge case in p2p networks! @@ -54,15 +68,32 @@ export default class Delete { return [] } } + + /** + * @private + * 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 {BinaryEncoder} encoder The encoder to write data to. + */ _toBinary (encoder) { encoder.writeUint8(getReference(this.constructor)) encoder.writeID(this._targetID) encoder.writeVarUint(this._length) } + /** - * - If created remotely (a remote user deleted something), + * @private + * Integrates this Item into the shared structure. + * + * This method actually applies the change to the Yjs instance. In the case of + * Delete it marks the delete target as deleted. + * + * * If created remotely (a remote user deleted something), * this Delete is applied to all structs in id-range. - * - If created lokally (e.g. when y-array deletes a range of elements), + * * If created lokally (e.g. when y-array deletes a range of elements), * this struct is broadcasted only (it is already executed) */ _integrate (y, locallyCreated = false) { @@ -78,6 +109,12 @@ export default class Delete { y.persistence.saveStruct(y, this) } } + + /** + * @private + * Transform this Delete to a readable format. + * Useful for logging as all Items implement this method. + */ _logString () { return `Delete - target: ${logID(this._targetID)}, len: ${this._length}` } diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 120c4e75..eba53d8a 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -5,11 +5,12 @@ import Delete from './Delete.js' import { transactionTypeChanged } from '../Transaction.js' /** - * Helper utility to split an Item (see _splitAt) - * - copy all properties from a to b - * - connect a to b + * @private + * Helper utility to split an Item (see {@link Item#_splitAt}) + * - copies all properties from a to b + * - connects a to b * - assigns the correct _id - * - save b to os + * - saves b to os */ export function splitHelper (y, a, b, diff) { const aID = a._id @@ -46,6 +47,10 @@ export function splitHelper (y, a, b, diff) { } } +/** + * @private + * Abstract class that represents any content. + */ export default class Item { constructor () { this._id = null @@ -58,14 +63,18 @@ export default class Item { this._deleted = false this._redone = null } + /** - * Create a operation with the same effect (without position effect) + * @private + * Creates an Item with the same effect as this Item (without position effect) */ _copy () { return new this.constructor() } + /** - * Redo the effect of this operation. + * @private + * Redoes the effect of this operation. */ _redo (y) { if (this._redone !== null) { @@ -106,27 +115,43 @@ export default class Item { return struct } + /** + * @private + * Computes the last content address of this Item. + */ get _lastId () { return new ID(this._id.user, this._id.clock + this._length - 1) } + + /** + * @private + * Computes the length of this Item. + */ get _length () { return 1 } + /** - * Some elements are not supposed to be addressable. For example, an - * ItemFormat should not be retrievable via yarray.get(pos) + * @private + * 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 */ get _countable () { return true } + /** - * Splits this struct so that another struct can be inserted in-between. + * @private + * Splits this Item so that another Items can be inserted in-between. * This must be overwritten if _length > 1 * Returns right part after split - * - diff === 0 => this - * - diff === length => this._right - * - otherwise => split _content and return right part of split - * (see ItemJSON/ItemString for implementation) + * * diff === 0 => this + * * diff === length => this._right + * * otherwise => split _content and return right part of split + * (see {@link ItemJSON}/{@link ItemString} for implementation) */ _splitAt (y, diff) { if (diff === 0) { @@ -134,6 +159,15 @@ export default class Item { } return this._right } + + /** + * @private + * Mark this Item as deleted. + * + * @param {Y} y The Yjs instance + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + */ _delete (y, createDelete = true) { if (!this._deleted) { this._deleted = true @@ -152,17 +186,27 @@ export default class Item { y._transaction.deletedStructs.add(this) } } + /** - * This is called right before this struct receives any children. + * @private + * This is called right before this Item receives any children. * It can be overwritten to apply pending changes before applying remote changes */ _beforeChange () { // nop } - /* - * - Integrate the struct so that other types/structs can see it - * - Add this struct to y.os - * - Check if this is struct deleted + + /** + * @private + * Integrates this Item into the shared structure. + * + * This method actually applies the change to the Yjs instance. In case of + * Item it connects _left and _right to this Item and calls the + * {@link Item#beforeChange} method. + * + * * Integrate the struct so that other types/structs can see it + * * Add this struct to y.os + * * Check if this is struct deleted */ _integrate (y) { y._transaction.newTypes.add(this) @@ -188,6 +232,7 @@ export default class Item { // or this types is new this._parent._beforeChange() } + /* # $this has to find a unique position between origin and the next known character # case 1: $origin equals $o.origin: the $creator parameter decides if left or right @@ -280,6 +325,16 @@ export default class Item { } } } + + /** + * @private + * 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 {BinaryEncoder} encoder The encoder to write data to. + */ _toBinary (encoder) { encoder.writeUint8(getReference(this.constructor)) let info = 0 @@ -320,6 +375,16 @@ export default class Item { encoder.writeVarString(JSON.stringify(this._parentSub)) } } + + /** + * @private + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {BinaryDecoder} decoder The decoder object to read data from. + */ _fromBinary (y, decoder) { let missing = [] const info = decoder.readUint8() diff --git a/src/Struct/Type.js b/src/Struct/Type.js index 80d93fed..a37aff51 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -30,6 +30,9 @@ export function getListItemIDByPosition (type, i) { } } +/** + * Abstract Yjs Type class + */ export default class Type extends Item { constructor () { super() @@ -39,6 +42,20 @@ export default class Type extends Item { this._eventHandler = new EventHandler() this._deepEventHandler = new EventHandler() } + + /** + * Compute the path from this type to the specified target. + * + * @example + * It should be accessible via `this.get(result[0]).get(result[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 {YType} type Type target + * @return {Array} Path to the target + */ getPathTo (type) { if (type === this) { return [] @@ -65,6 +82,12 @@ export default class Type extends Item { } return path } + + /** + * @private + * Call event listeners with an event. This will also add an event to all + * parents (for `.observeDeep` handlers). + */ _callEventHandler (transaction, event) { const changedParentTypes = transaction.changedParentTypes this._eventHandler.callEventListeners(transaction, event) @@ -79,6 +102,14 @@ export default class Type extends Item { type = type._parent } } + + /** + * @private + * Helper method to transact if the y instance is available. + * + * TODO: Currently event handlers are not thrown when a type is not registered + * with a Yjs instance. + */ _transact (f) { const y = this._y if (y !== null) { @@ -87,18 +118,53 @@ export default class Type extends Item { f(y) } } + + /** + * Observe all events that are created on this type. + * + * @param {Function} f Observer function + */ observe (f) { this._eventHandler.addEventListener(f) } + + /** + * Observe all events that are created by this type and its children. + * + * @param {Function} f Observer function + */ observeDeep (f) { this._deepEventHandler.addEventListener(f) } + + /** + * Unregister an observer function. + * + * @param {Function} f Observer function + */ unobserve (f) { this._eventHandler.removeEventListener(f) } + + /** + * Unregister an observer function. + * + * @param {Function} f Observer function + */ unobserveDeep (f) { this._deepEventHandler.removeEventListener(f) } + + /** + * @private + * 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 {Y} y The Yjs instance + */ _integrate (y) { super._integrate(y) this._y = y @@ -117,6 +183,15 @@ export default class Type extends Item { integrateChildren(y, t) } } + + /** + * @private + * Mark this Item as deleted. + * + * @param {Y} y The Yjs instance + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + */ _delete (y, createDelete) { super._delete(y, createDelete) y._transaction.changedTypes.delete(this) diff --git a/src/Transaction.js b/src/Transaction.js index 6a99a96b..4b372fdf 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -1,4 +1,12 @@ +/** + * Changes that are created within a transaction are bundled and sent as one + * message to the remote peers. This implies that the changes are applied + * in one flush and at most one {@link YEvent} per type is created. + * + * It is best to bundle as many changes in a single Transaction as possible. + * This way only few changes need to be computed + */ export default class Transaction { constructor (y) { this.y = y diff --git a/src/Type/YArray.js b/src/Type/YArray.js index b017266b..5ef5e50a 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -4,6 +4,13 @@ import ItemString from '../Struct/ItemString.js' import { logID } from '../MessageHandler/messageToString.js' import YEvent from '../Util/YEvent.js' +/** + * Event that describes the changes on a YArray + * + * @param {YArray} yarray The changed type + * @param {Boolean} remote Whether the changed was caused by a remote peer + * @param {Transaction} transaction The transaction object + */ export class YArrayEvent extends YEvent { constructor (yarray, remote, transaction) { super(yarray) @@ -12,6 +19,12 @@ export class YArrayEvent extends YEvent { this._addedElements = null this._removedElements = null } + + /** + * Child elements that were added in this transaction. + * + * @return {Set} + */ get addedElements () { if (this._addedElements === null) { const target = this.target @@ -26,6 +39,12 @@ export class YArrayEvent extends YEvent { } return this._addedElements } + + /** + * Child elements that were removed in this transaction. + * + * @return {Set} + */ get removedElements () { if (this._removedElements === null) { const target = this.target @@ -42,29 +61,54 @@ export class YArrayEvent extends YEvent { } } +/** + * A shared Array implementation. + */ export default class YArray extends Type { + /** + * @private + * Creates YArray Event and calls observers. + */ _callObserver (transaction, parentSubs, remote) { this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction)) } - get (pos) { + + /** + * Returns the i-th element from a YArray. + * + * @param {Integer} index The index of the element to return from the YArray + */ + get (index) { let n = this._start while (n !== null) { if (!n._deleted && n._countable) { - if (pos < n._length) { + if (index < n._length) { if (n.constructor === ItemJSON || n.constructor === ItemString) { - return n._content[pos] + return n._content[index] } else { return n } } - pos -= n._length + index -= n._length } n = n._right } } + + /** + * Transforms this YArray to a JavaScript Array. + * + * @return {Array} + */ toArray () { return this.map(c => c) } + + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Array} + */ toJSON () { return this.map(c => { if (c instanceof Type) { @@ -77,6 +121,15 @@ export default class YArray extends Type { return c }) } + + /** + * Returns an Array with the result of calling a provided function on every + * element of this YArray. + * + * @param {Function} 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) { const res = [] this.forEach((c, i) => { @@ -84,25 +137,35 @@ export default class YArray extends Type { }) return res } + + /** + * Executes a provided function on once on overy element of this YArray. + * + * @param {Function} f A function to execute on every element of this YArray. + */ forEach (f) { - let pos = 0 + let index = 0 let n = this._start while (n !== null) { if (!n._deleted && n._countable) { if (n instanceof Type) { - f(n, pos++, this) + f(n, index++, this) } else { const content = n._content const contentLen = content.length for (let i = 0; i < contentLen; i++) { - pos++ - f(content[i], pos, this) + index++ + f(content[i], index, this) } } } n = n._right } } + + /** + * Computes the length of this YArray. + */ get length () { let length = 0 let n = this._start @@ -114,6 +177,7 @@ export default class YArray extends Type { } return length } + [Symbol.iterator] () { return { next: function () { @@ -143,14 +207,21 @@ export default class YArray extends Type { _count: 0 } } - delete (pos, length = 1) { + + /** + * Deletes elements starting from an index. + * + * @param {Integer} index Index at which to start deleting elements + * @param {Integer} length The number of elements to remove. Defaults to 1. + */ + delete (index, length = 1) { this._y.transact(() => { let item = this._start let count = 0 while (item !== null && length > 0) { if (!item._deleted && item._countable) { - if (count <= pos && pos < count + item._length) { - const diffDel = pos - count + if (count <= index && index < count + item._length) { + const diffDel = index - count item = item._splitAt(this._y, diffDel) item._splitAt(this._y, length) length -= item._length @@ -167,6 +238,14 @@ export default class YArray extends Type { throw new Error('Delete exceeds the range of the YArray') } } + + /** + * @private + * Inserts content after an element container. + * + * @param {Item} left The element container to use as a reference. + * @param {Array} content The Array of content to insert (see {@see insert}) + */ insertAfter (left, content) { this._transact(y => { let right @@ -224,7 +303,24 @@ export default class YArray extends Type { } }) } - insert (pos, content) { + + /** + * 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(2, [1, 2]) + * + * @param {Integer} index The index to insert content at. + * @param {Array} content The array of content + */ + insert (index, content) { this._transact(() => { let left = null let right = this._start @@ -232,8 +328,8 @@ export default class YArray extends Type { const y = this._y while (right !== null) { const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= pos && pos <= count + rightLen) { - const splitDiff = pos - count + if (count <= index && index <= count + rightLen) { + const splitDiff = index - count right = right._splitAt(y, splitDiff) left = right._left count += splitDiff @@ -245,12 +341,18 @@ export default class YArray extends Type { left = right right = right._right } - if (pos > count) { - throw new Error('Position exceeds array range!') + if (index > count) { + throw new Error('Index exceeds array range!') } this.insertAfter(left, content) }) } + + /** + * Appends content to this YArray. + * + * @param {Array} content Array of content to append. + */ push (content) { let n = this._start let lastUndeleted = null @@ -262,6 +364,12 @@ export default class YArray extends Type { } this.insertAfter(lastUndeleted, content) } + + /** + * @private + * Transform this YArray to a readable format. + * Useful for logging as all Items implement this method. + */ _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null diff --git a/src/Type/YMap.js b/src/Type/YMap.js index 06391571..6103b127 100644 --- a/src/Type/YMap.js +++ b/src/Type/YMap.js @@ -4,7 +4,14 @@ import ItemJSON from '../Struct/ItemJSON.js' import { logID } from '../MessageHandler/messageToString.js' import YEvent from '../Util/YEvent.js' -class YMapEvent extends YEvent { +/** + * Event that describes the changes on a YMap. + * + * @param {YMap} ymap The YArray that changed. + * @param {Set} subs The keys that changed. + * @param {boolean} remote Whether the change was created by a remote peer. + */ +export class YMapEvent extends YEvent { constructor (ymap, subs, remote) { super(ymap) this.keysChanged = subs @@ -12,10 +19,23 @@ class YMapEvent extends YEvent { } } +/** + * A shared Map implementation. + */ export default class YMap extends Type { + /** + * @private + * Creates YMap Event and calls observers. + */ _callObserver (transaction, parentSubs, remote) { this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote)) } + + /** + * Transforms this Shared Type to a JSON object. + * + * @return {Object} + */ toJSON () { const map = {} for (let [key, item] of this._map) { @@ -35,7 +55,14 @@ export default class YMap extends Type { } return map } + + /** + * Returns the keys for each element in the YMap Type. + * + * @return {Array} + */ keys () { + // TODO: Should return either Iterator or Set! let keys = [] for (let [key, value] of this._map) { if (!value._deleted) { @@ -44,6 +71,12 @@ export default class YMap extends Type { } return keys } + + /** + * Remove a specified element from this YMap. + * + * @param {encodable} key The key of the element to remove. + */ delete (key) { this._transact((y) => { let c = this._map.get(key) @@ -52,11 +85,22 @@ export default class YMap extends Type { } }) } + + /** + * Adds or updates an element with a specified key and value. + * + * @param {encodable} key The key of the element to add to this YMap. + * @param {encodable | YType} value The value of the element to add to this + * YMap. + */ set (key, value) { this._transact(y => { const old = this._map.get(key) || null if (old !== null) { - if (old.constructor === ItemJSON && !old._deleted && old._content[0] === value) { + if ( + old.constructor === ItemJSON && + !old._deleted && old._content[0] === value + ) { // Trying to overwrite with same value // break here return value @@ -87,6 +131,12 @@ export default class YMap extends Type { }) return value } + + /** + * Returns a specified element from this YMap. + * + * @param {encodable} key The key of the element to return. + */ get (key) { let v = this._map.get(key) if (v === undefined || v._deleted) { @@ -98,6 +148,12 @@ export default class YMap extends Type { return v._content[v._content.length - 1] } } + + /** + * Returns a boolean indicating whether the specified key exists or not. + * + * @param {encodable} key The key to test. + */ has (key) { let v = this._map.get(key) if (v === undefined || v._deleted) { @@ -106,6 +162,12 @@ export default class YMap extends Type { return true } } + + /** + * @private + * Transform this YMap to a readable format. + * Useful for logging as all Items implement this method. + */ _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null diff --git a/src/Type/YText.js b/src/Type/YText.js index 926d8ef7..3f565e53 100644 --- a/src/Type/YText.js +++ b/src/Type/YText.js @@ -46,11 +46,11 @@ function findNextPosition (currentAttributes, parent, left, right, count) { return [left, right, currentAttributes] } -function findPosition (parent, pos) { +function findPosition (parent, index) { let currentAttributes = new Map() let left = null let right = parent._start - return findNextPosition(currentAttributes, parent, left, right, pos) + return findNextPosition(currentAttributes, parent, left, right, index) } // negate applied formats @@ -212,11 +212,50 @@ function deleteText (y, length, parent, left, right, currentAttributes) { return [left, right] } +// TODO: In the quill delta representation we should also use the format {ops:[..]} +/** + * 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' } } + * ] + * } + * + * @typedef {Array} Delta + */ + + /** + * Attributes that can be assigned to a selection of text. + * + * @example + * { + * bold: true, + * font-size: '40px' + * } + * + * @typedef {Object} TextAttributes + */ + +/** + * Event that describes the changes on a YText type. + */ class YTextEvent extends YArrayEvent { constructor (ytext, remote, transaction) { super(ytext, remote, transaction) this._delta = null } + + /** + * Compute the changes in the delta format. + * + * @return {Delta} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that + * represents the changes on the document. + */ get delta () { if (this._delta === null) { const y = this.target._y @@ -378,6 +417,15 @@ class YTextEvent extends YArrayEvent { } } +/** + * 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*). + * + * @param {String} string The initial value of the YText. + */ export default class YText extends YArray { constructor (string) { super() @@ -388,9 +436,18 @@ export default class YText extends YArray { this._start = start } } + + /** + * @private + * Creates YMap Event and calls observers. + */ _callObserver (transaction, parentSubs, remote) { this._callEventHandler(transaction, new YTextEvent(this, remote, transaction)) } + + /** + * Returns the unformatted string representation of this YText type. + */ toString () { let str = '' let n = this._start @@ -402,6 +459,12 @@ export default class YText extends YArray { } return str } + + /** + * Apply a {@link Delta} on this shared YText type. + * + * @param {Delta} delta The changes to apply on this element. + */ applyDelta (delta) { this._transact(y => { let left = null @@ -419,8 +482,11 @@ export default class YText extends YArray { } }) } + /** - * As defined by Quilljs - https://quilljs.com/docs/delta/ + * Returns the Delta representation of this YText type. + * + * @return {Delta} The Delta representation of this type. */ toDelta () { let ops = [] @@ -461,42 +527,84 @@ export default class YText extends YArray { packStr() return ops } - insert (pos, text, attributes = {}) { + + /** + * Insert text at a given index. + * + * @param {Integer} 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. + */ + insert (index, text, attributes = {}) { if (text.length <= 0) { return } this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos) + let [left, right, currentAttributes] = findPosition(this, index) insertText(y, text, this, left, right, currentAttributes, attributes) }) } - insertEmbed (pos, embed, attributes = {}) { + + /** + * Inserts an embed at a index. + * + * @param {Integer} index The index to insert the embed at. + * @param {Object} embed The Object that represents the embed. + * @param {TextAttributes} attributes Attribute information to apply on the + * embed + * + */ + insertEmbed (index, embed, attributes = {}) { if (embed.constructor !== Object) { throw new Error('Embed must be an Object') } this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos) + let [left, right, currentAttributes] = findPosition(this, index) insertText(y, embed, this, left, right, currentAttributes, attributes) }) } - delete (pos, length) { + + /** + * Deletes text starting from an index. + * + * @param {Integer} index Index at which to start deleting. + * @param {Integer} length The number of characters to remove. Defaults to 1. + */ + delete (index, length) { if (length === 0) { return } this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos) + let [left, right, currentAttributes] = findPosition(this, index) deleteText(y, length, this, left, right, currentAttributes) }) } - format (pos, length, attributes) { + + /** + * Assigns properties to a range of text. + * + * @param {Integer} index The position where to start formatting. + * @param {Integer} length The amount of characters to assign properties to. + * @param {TextAttributes} attributes Attribute information to apply on the + * text. + */ + format (index, length, attributes) { this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos) + let [left, right, currentAttributes] = findPosition(this, index) if (right === null) { return } formatText(y, length, this, left, right, currentAttributes, attributes) }) } + + /** + * @private + * Transform this YText to a readable format. + * Useful for logging as all Items implement this method. + */ _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null diff --git a/src/Type/y-xml/YXmlElement.js b/src/Type/y-xml/YXmlElement.js index c795879c..bbfaf7c3 100644 --- a/src/Type/y-xml/YXmlElement.js +++ b/src/Type/y-xml/YXmlElement.js @@ -3,6 +3,16 @@ import { defaultDomFilter } from './utils.js' import YMap from '../YMap.js' import { YXmlFragment } from './y-xml.js' +/** + * 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 + * + * @param {String} arg1 Node name + * @param {Function} arg2 Dom filter + */ export default class YXmlElement extends YXmlFragment { constructor (arg1, arg2, _document) { super() @@ -20,11 +30,21 @@ export default class YXmlElement extends YXmlFragment { this._domFilter = arg2 } } + + /** + * @private + * Creates an Item with the same effect as this Item (without position effect) + */ _copy () { let struct = super._copy() struct.nodeName = this.nodeName return struct } + + /** + * @private + * Copies children and attributes from a dom node to this YXmlElement. + */ _setDom (dom, _document) { if (this._dom != null) { throw new Error('Only call this method if you know what you are doing ;)') @@ -48,20 +68,61 @@ export default class YXmlElement extends YXmlFragment { return dom } } + + /** + * @private + * Bind a dom to to this YXmlElement. This means that the DOM changes when the + * YXmlElement is modified and that this YXmlElement changes when the DOM is + * modified. + * + * Currently only works in YXmlFragment. + */ _bindToDom (dom, _document) { _document = _document || document this._dom = dom dom._yxml = this } + + /** + * @private + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {BinaryDecoder} decoder The decoder object to read data from. + */ _fromBinary (y, decoder) { const missing = super._fromBinary(y, decoder) this.nodeName = decoder.readVarString() return missing } + + /** + * @private + * 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 {BinaryEncoder} encoder The encoder to write data to. + */ _toBinary (encoder) { super._toBinary(encoder) encoder.writeVarString(this.nodeName) } + + /** + * @private + * Integrates this Item into the shared structure. + * + * This method actually applies the change to the Yjs instance. In case of + * Item it connects _left and _right to this Item and calls the + * {@link Item#beforeChange} method. + * + * * Checks for nodeName + * * Sets domFilter + */ _integrate (y) { if (this.nodeName === null) { throw new Error('nodeName must be defined!') @@ -71,8 +132,9 @@ export default class YXmlElement extends YXmlFragment { } super._integrate(y) } + /** - * Returns the string representation of the XML document. + * Returns the string representation of this YXmlElement. * The attributes are ordered by attribute-name, so you can easily use this * method to compare YXmlElements */ @@ -93,18 +155,42 @@ export default class YXmlElement extends YXmlFragment { 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. + */ removeAttribute () { return YMap.prototype.delete.apply(this, arguments) } + /** + * Sets or updates an attribute. + * + * @param {String} attributeName The attribute name that is to be set. + * @param {String} attributeValue The attribute value that is to be set. + */ setAttribute () { return YMap.prototype.set.apply(this, arguments) } + /** + * Returns an attribute value that belongs to the attribute name. + * + * @param {String} attributeName The attribute name that identifies the + * queried value. + * @return {String} The queried attribute value + */ getAttribute () { return YMap.prototype.get.apply(this, arguments) } + /** + * Returns all attribute name/value pairs in a JSON Object. + * + * @return {Object} A JSON Object that describes the attributes. + */ getAttributes () { const obj = {} for (let [key, value] of this._map) { @@ -114,6 +200,12 @@ export default class YXmlElement extends YXmlFragment { } return obj } + + /** + * Creates a Dom Element that mirrors this YXmlElement. + * + * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} + */ getDom (_document) { _document = _document || document let dom = this._dom diff --git a/src/Type/y-xml/YXmlEvent.js b/src/Type/y-xml/YXmlEvent.js index b63f300d..b27eb139 100644 --- a/src/Type/y-xml/YXmlEvent.js +++ b/src/Type/y-xml/YXmlEvent.js @@ -1,5 +1,8 @@ import YEvent from '../../Util/YEvent.js' +/** + * An Event that describes changes on a YXml Element or Yxml Fragment + */ export default class YXmlEvent extends YEvent { constructor (target, subs, remote) { super(target) diff --git a/src/Type/y-xml/YXmlFragment.js b/src/Type/y-xml/YXmlFragment.js index ace6b680..96663f03 100644 --- a/src/Type/y-xml/YXmlFragment.js +++ b/src/Type/y-xml/YXmlFragment.js @@ -36,6 +36,24 @@ function domToYXml (parent, doms, _document) { return types } +/** + * 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 + */ + +/** + * Represents a subset of the nodes of a YXmlElement / YXmlFragment and a + * position within them. + * + * Can be created with {@link YXmlFragment#createTreeWalker} + */ class YXmlTreeWalker { constructor (root, f) { this._filter = f || (() => true) @@ -46,6 +64,11 @@ class YXmlTreeWalker { [Symbol.iterator] () { return this } + /** + * Get the next node. + * + * @return {YXmlElement} The next node. + */ next () { let n = this._currentNode if (this._firstCall) { @@ -84,6 +107,11 @@ class YXmlTreeWalker { } } +/** + * Represents a list of {@link YXmlElement}. + * A YxmlFragment does not have a nodeName and it does not have attributes. + * Therefore it also must not be added as a childElement. + */ export default class YXmlFragment extends YArray { constructor () { super() @@ -110,18 +138,31 @@ export default class YXmlFragment extends YArray { } } } + + /** + * Create a subtree of childNodes. + * + * @param {Function} 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 {TreeWalker} A subtree and a position within it. + */ createTreeWalker (filter) { return new YXmlTreeWalker(this, filter) } + /** - * Retrieve first element that matches *query* - * Similar to DOM's querySelector, but only accepts a subset of its queries + * 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} The first element that matches the query or null. */ querySelector (query) { query = query.toUpperCase() @@ -133,16 +174,52 @@ export default class YXmlFragment extends YArray { 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. + */ querySelectorAll (query) { query = query.toUpperCase() return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query)) } + + /** + * Enables the smart scrolling functionality for a Dom Binding. + * This is useful when YXml is bound to a shared editor. When activated, + * the viewport will be changed to accommodate remote changes. + * + * @TODO: Disabled for now. + * + * @param {Element} scrollElement The node that is + */ enableSmartScrolling (scrollElement) { this._scrollElement = scrollElement this.forEach(xml => { xml.enableSmartScrolling(scrollElement) }) } + + /** + * 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. + */ + + /** + * Filter out Dom elements. + * + * @param {domFilter} f The filtering function that decides whether to include + * a Dom node. + */ setDomFilter (f) { this._domFilter = f let attributes = new Map() @@ -168,16 +245,41 @@ export default class YXmlFragment extends YArray { }) }) } + + /** + * @private + * Creates YArray Event and calls observers. + */ _callObserver (transaction, parentSubs, remote) { this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote)) } + + /** + * Get the string representation of all the children of this YXmlFragment. + * + * @return {string} The string representation of all children. + */ toString () { return this.map(xml => xml.toString()).join('') } + + /** + * @private + * Unbind from Dom and mark this Item as deleted. + * + * @param {Y} y The Yjs instance + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + */ _delete (y, createDelete) { this._unbindFromDom() super._delete(y, createDelete) } + + /** + * @private + * Unbind this YXmlFragment from the Dom. + */ _unbindFromDom () { if (this._domObserver != null) { this._domObserver.disconnect() @@ -191,19 +293,53 @@ export default class YXmlFragment extends YArray { this._y.off('beforeTransaction', this._beforeTransactionHandler) } } + + /** + * Insert Dom Elements after one of the children of this YXmlFragment. + * The Dom elements will be bound to a new YXmlElement and inserted at the + * specified position. + * + * @param {YXmlElement|null} prev The reference node. New YxmlElements are + * inserted after this node. Set null to insert at + * the beginning. + * @param {Array} doms The Dom elements to insert. + * @return {Array} The YxmlElements that are inserted. + */ insertDomElementsAfter (prev, doms, _document) { const types = domToYXml(this, doms, _document) this.insertAfter(prev, types) return types } - insertDomElements (pos, doms, _document) { + + /** + * Insert Dom Elements at a specified index. + * The Dom elements will be bound to a new YXmlElement and inserted at the + * specified position. + * + * @param {Integer} index The position to insert elements at. + * @param {Array} doms The Dom elements to insert. + * @return {Array} The YxmlElements that are inserted. + */ + insertDomElements (index, doms, _document) { const types = domToYXml(this, doms, _document) - this.insert(pos, types) + this.insert(index, types) return types } + + /** + * Get the Dom representation of this YXml type.. + */ getDom () { return this._dom } + + /** + * Bind this YXmlFragment and all its children to a Dom Element. + * The content of the Dom Element are replaced with the Dom representation of + * the children of this YXml Type. + * + * @param {Element} dom The Dom Element that should be bound to this Type. + */ bindToDom (dom, _document) { if (this._dom != null) { this._unbindFromDom() @@ -217,8 +353,12 @@ export default class YXmlFragment extends YArray { }) this._bindToDom(dom, _document) } - // binds to a dom element - // Only call if dom and YXml are isomorph + + /** + * @private + * Binds to a dom element. + * Only call if dom and YXml are isomorph + */ _bindToDom (dom, _document) { _document = _document || document this._dom = dom @@ -346,6 +486,12 @@ export default class YXmlFragment extends YArray { } return dom } + + /** + * @private + * Transform this YXml Type to a readable format. + * Useful for logging as all Items implement this method. + */ _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null diff --git a/src/Type/y-xml/YXmlHook.js b/src/Type/y-xml/YXmlHook.js index ee2ce4f7..a5834ab2 100644 --- a/src/Type/y-xml/YXmlHook.js +++ b/src/Type/y-xml/YXmlHook.js @@ -1,6 +1,11 @@ import YMap from '../YMap.js' import { getHook, addHook } from './hooks.js' +/** + * You can manage binding to a custom type with YXmlHook. + * + * @param {String} hookName nodeName of the Dom Node. + */ export default class YXmlHook extends YMap { constructor (hookName, dom) { super() @@ -14,11 +19,20 @@ export default class YXmlHook extends YMap { getHook(hookName).fillType(dom, this) } } + + /** + * @private + * Creates an Item with the same effect as this Item (without position effect) + */ _copy () { const struct = super._copy() struct.hookName = this.hookName return struct } + + /** + * Returns the Dom representation of this YXmlHook. + */ getDom (_document) { _document = _document || document if (this._dom === null) { @@ -29,20 +43,56 @@ export default class YXmlHook extends YMap { } return this._dom } + + /** + * @private + * Removes the Dom binding. + */ _unbindFromDom () { this._dom._yxml = null this._yxml = null // TODO: cleanup hook? } + + /** + * @private + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {BinaryDecoder} decoder The decoder object to read data from. + */ _fromBinary (y, decoder) { const missing = super._fromBinary(y, decoder) this.hookName = decoder.readVarString() return missing } + + /** + * @private + * 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 {BinaryEncoder} encoder The encoder to write data to. + */ _toBinary (encoder) { super._toBinary(encoder) encoder.writeVarString(this.hookName) } + + /** + * @private + * 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 {Y} y The Yjs instance + */ _integrate (y) { if (this.hookName === null) { throw new Error('hookName must be defined!') diff --git a/src/Type/y-xml/YXmlText.js b/src/Type/y-xml/YXmlText.js index 7fbd48d5..0e4eb78c 100644 --- a/src/Type/y-xml/YXmlText.js +++ b/src/Type/y-xml/YXmlText.js @@ -1,5 +1,11 @@ import YText from '../YText.js' +/** + * Represents text in a Dom Element. In the future this type will also handle + * simple formatting information like bold and italic. + * + * @param {String} arg1 Initial value. + */ export default class YXmlText extends YText { constructor (arg1) { let dom = null @@ -56,6 +62,15 @@ export default class YXmlText extends YText { enableSmartScrolling (scrollElement) { this._scrollElement = scrollElement } + + /** + * @private + * Set Dom element / Text Node that represents the same content as this + * YXmlElement. + * + * @param {Element} dom The Dom Element / Text Node that is set to be + * equivalent to this Type. + */ _setDom (dom) { if (this._dom != null) { this._unbindFromDom() @@ -67,6 +82,10 @@ export default class YXmlText extends YText { this._dom = dom dom._yxml = this } + + /** + * Returns the Dom representation of this YXmlText. + */ getDom (_document) { _document = _document || document if (this._dom === null) { @@ -76,10 +95,24 @@ export default class YXmlText extends YText { } return this._dom } + + /** + * @private + * Mark this Item as deleted. + * + * @param {Y} y The Yjs instance + * @param {boolean} createDelete Whether to propagate a message that this + * Type was deleted. + */ _delete (y, createDelete) { this._unbindFromDom() super._delete(y, createDelete) } + + /** + * @private + * Unbind this YXmlText from the Dom. + */ _unbindFromDom () { if (this._domObserver != null) { this._domObserver.disconnect() diff --git a/src/Type/y-xml/utils.js b/src/Type/y-xml/utils.js index 0e1b23d0..0f30647c 100644 --- a/src/Type/y-xml/utils.js +++ b/src/Type/y-xml/utils.js @@ -184,8 +184,8 @@ export function reflectChangesOnDom (events, _document) { dom.setAttribute(attributeName, value) } }) - /** - * TODO: instead of chard-checking the types, it would be best to + /* + * TODO: instead of hard-checking the types, it would be best to * specify the type's features. E.g. * - _yxmlHasAttributes * - _yxmlHasChildren diff --git a/src/Util/EventHandler.js b/src/Util/EventHandler.js index ab3dbe4c..c82c0f46 100644 --- a/src/Util/EventHandler.js +++ b/src/Util/EventHandler.js @@ -1,22 +1,56 @@ +/** + * General event handler implementation. + */ export default class EventHandler { constructor () { this.eventListeners = [] } + + /** + * To prevent memory leaks, call this method when the eventListeners won't be + * used anymore. + */ destroy () { this.eventListeners = null } + + /** + * Adds an event listener that is called when + * {@link EventHandler#callEventListeners} is called. + * + * @param {Function} f The event handler. + */ addEventListener (f) { this.eventListeners.push(f) } + + /** + * Removes an event listener. + * + * @param {Function} f The event handler that was added with + * {@link EventHandler#addEventListener} + */ removeEventListener (f) { this.eventListeners = this.eventListeners.filter(function (g) { return f !== g }) } + + /** + * Removes all event listeners. + */ removeAllEventListeners () { this.eventListeners = [] } + + /** + * Call all event listeners that were added via + * {@link EventHandler#addEventListener}. + * + * @param {Transaction} transaction The transaction object // TODO: do we need this? + * @param {YEvent} event An event object that describes the change on a type. + */ callEventListeners (transaction, event) { for (var i = 0; i < this.eventListeners.length; i++) { try { diff --git a/src/Util/NamedEventHandler.js b/src/Util/NamedEventHandler.js index 632d8321..8eee778c 100644 --- a/src/Util/NamedEventHandler.js +++ b/src/Util/NamedEventHandler.js @@ -1,8 +1,19 @@ + +/** + * Handles named events. + */ export default class NamedEventHandler { constructor () { this._eventListener = new Map() this._stateListener = new Map() } + + /** + * @private + * Returns all listeners that listen to a specified name. + * + * @param {String} name The query event name. + */ _getListener (name) { let listeners = this._eventListener.get(name) if (listeners === undefined) { @@ -14,14 +25,34 @@ export default class NamedEventHandler { } return listeners } + + /** + * Adds a named event listener. The listener is removed after it has been + * called once. + * + * @param {String} name The event name to listen to. + * @param {Function} f The function that is executed when the event is fired. + */ once (name, f) { let listeners = this._getListener(name) listeners.once.add(f) } + + /** + * Adds a named event listener. + * + * @param {String} name The event name to listen to. + * @param {Function} f The function that is executed when the event is fired. + */ on (name, f) { let listeners = this._getListener(name) listeners.on.add(f) } + + /** + * @private + * Init the saved state for an event name. + */ _initStateListener (name) { let state = this._stateListener.get(name) if (state === undefined) { @@ -33,9 +64,20 @@ export default class NamedEventHandler { } return state } + + /** + * Returns a Promise that is resolved when the event name is called. + * The Promise is immediately resolved when the event name was called in the + * past. + */ when (name) { return this._initStateListener(name).promise } + + /** + * Remove an event listener that was registered with either + * {@link EventHandler#on} or {@link EventHandler#once}. + */ off (name, f) { if (name == null || f == null) { throw new Error('You must specify event name and function!') @@ -46,6 +88,14 @@ export default class NamedEventHandler { listener.once.delete(f) } } + + /** + * Emit a named event. All registered event listeners that listen to the + * specified name will receive the event. + * + * @param {String} name The event name. + * @param {Array} args The arguments that are applied to the event listener. + */ emit (name, ...args) { this._initStateListener(name).resolve() const listener = this._eventListener.get(name) diff --git a/src/Util/UndoManager.js b/src/Util/UndoManager.js index c18c36d3..963b45d5 100644 --- a/src/Util/UndoManager.js +++ b/src/Util/UndoManager.js @@ -64,7 +64,15 @@ function applyReverseOperation (y, scope, reverseBuffer) { return performedUndo } +/** + * Saves a history of locally applied operations. The UndoManager handles the + * undoing and redoing of locally created changes. + */ export default class UndoManager { + /** + * @param {YType} scope The scope on which to listen for changes. + * @param {Object} options Optionally provided configuration. + */ constructor (scope, options = {}) { this.options = options options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout @@ -101,12 +109,20 @@ export default class UndoManager { } }) } + + /** + * Undo the last locally created change. + */ undo () { this._undoing = true const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer) this._undoing = false return performedUndo } + + /** + * Redo the last locally created change. + */ redo () { this._redoing = true const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer) diff --git a/src/Util/YEvent.js b/src/Util/YEvent.js index aed8c4e8..1af22be4 100644 --- a/src/Util/YEvent.js +++ b/src/Util/YEvent.js @@ -1,9 +1,27 @@ +/** + * YEvent describes the changes on a YType. + */ export default class YEvent { + /** + * @param {YType} target The changed type. + */ constructor (target) { this.target = target this.currentTarget = target } + + /** + * Computes the path from `y` to the changed type. + * + * The following property holds: + * @example + * let type = y + * event.path.forEach(function (dir) { + * type = type.get(dir) + * }) + * type === event.target // => true + */ get path () { const path = [] let type = this.target diff --git a/src/Util/relativePosition.js b/src/Util/relativePosition.js index 77118b7f..cd824b67 100644 --- a/src/Util/relativePosition.js +++ b/src/Util/relativePosition.js @@ -1,7 +1,43 @@ import ID from './ID.js' import RootID from './RootID.js' +/** + * A relative position that is based on the Yjs model. In contrast to an + * absolute position (position by index), the relative position can be + * recomputed when remote changes are received. For example: + * + * ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position. + * + * A relative cursor position can be obtained with the function + * {@link getRelativePosition} and it can be transformed to an absolute position + * with {@link fromRelativePosition}. + * + * Pro tip: Use this to implement shared cursor locations in YText or YXml! + * The relative position is {@link encodable}, so you can send it to other + * clients. + * + * @example + * // Current cursor position is at position 10 + * let relativePosition = getRelativePosition(yText, 10) + * // modify yText + * yText.insert(0, 'abc') + * yText.delete(3, 10) + * // Compute the cursor position + * let absolutePosition = fromRelativePosition(y, relativePosition) + * absolutePosition.type // => yText + * console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3 + * + * @typedef {encodable} RelativePosition + */ + +/** + * Create a relativePosition based on a absolute position. + * + * @param {YType} type The base type (e.g. YText or YArray). + * @param {Integer} offset The absolute position. + */ export function getRelativePosition (type, offset) { + // TODO: rename to createRelativePosition let t = type._start while (t !== null) { if (t._deleted === false) { @@ -15,6 +51,20 @@ export function getRelativePosition (type, offset) { return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null] } +/** + * @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition} + * @property {YType} type The type on which to apply the absolute position. + * @property {Integer} offset The absolute offset.r + */ + +/** + * Transforms a relative position back to a relative position. + * + * @param {Y} y The Yjs instance in which to query for the absolute position. + * @param {RelativePosition} rpos The relative position. + * @return {AbsolutePosition} The absolute position in the Yjs model + * (type + offset). + */ export function fromRelativePosition (y, rpos) { if (rpos[0] === 'endof') { let id diff --git a/src/Y.js b/src/Y.js index fd29a6e9..55ff5cdc 100644 --- a/src/Y.js +++ b/src/Y.js @@ -27,9 +27,32 @@ import QuillBinding from './Binding/QuillBinding.js' import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' +/** + * Anything that can be encoded with `JSON.stringify` and can be decoded with + * `JSON.parse`. + * + * The following property should hold: + * `JSON.parse(JSON.stringify(key))===key` + * + * At the moment the only safe values are number and string. + * + * @typedef {(number|string)} encodable + */ + +/** + * A Yjs instance handles the state of shared data. + * + * @param {string} room Users in the same room share the same content + * @param {Object} opts Connector definition + * @param {AbstractPersistence} persistence Persistence adapter instance + */ export default class Y extends NamedEventHandler { constructor (room, opts, persistence) { super() + /** + * The room name that this Yjs instance connects to. + * @type {String} + */ this.room = room if (opts != null) { opts.connector.room = room @@ -37,6 +60,7 @@ export default class Y extends NamedEventHandler { this._contentReady = false this._opts = opts this.userID = generateUserID() + // TODO: This should be a Map so we can use encodables as keys this.share = {} this.ds = new DeleteStore(this) this.os = new OperationStore(this) @@ -44,6 +68,10 @@ export default class Y extends NamedEventHandler { this._missingStructs = new Map() this._readyToIntegrate = [] this._transaction = null + /** + * The {@link AbstractConnector}.that is used by this Yjs instance. + * @type {AbstractConnector} + */ this.connector = null this.connected = false let initConnection = () => { @@ -53,11 +81,15 @@ export default class Y extends NamedEventHandler { this.emit('connectorReady') } } + /** + * The {@link AbstractPersistence} that is used by this Yjs instance. + * @type {AbstractPersistence} + */ + this.persistence = null if (persistence != null) { this.persistence = persistence persistence._init(this).then(initConnection) } else { - this.persistence = null initConnection() } } @@ -77,6 +109,14 @@ export default class Y extends NamedEventHandler { } } _beforeChange () {} + /** + * 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. + * + * @param {Function} f The function that should be executed as a transaction + */ transact (f, remote = false) { let initialCall = this._transaction === null if (initialCall) { @@ -117,13 +157,55 @@ export default class Y extends NamedEventHandler { this.emit('afterTransaction', this, transaction, remote) } } - // fake _start for root properties (y.set('name', type)) + + /** + * @private + * Fake _start for root properties (y.set('name', type)) + */ get _start () { return null } + + /** + * @private + * Fake _start for root properties (y.set('name', type)) + */ set _start (start) { return null } + + /** + * Define a shared data type. + * + * Multiple calls of `y.define(name, TypeConstructor)` yield the same result + * and do not overwrite each other. I.e. + * `y.define(name, type) === y.define(name, type)` + * + * After this method is called, the type is also available on `y.share[name]`. + * + * *Best Practices:* + * Either define all types right after the Yjs instance is created or always + * use `y.define(..)` when accessing a type. + * + * @example + * // Option 1 + * const y = new Y(..) + * y.define('myArray', YArray) + * y.define('myMap', YMap) + * // .. when accessing the type use y.share[name] + * y.share.myArray.insert(..) + * y.share.myMap.set(..) + * + * // Option2 + * const y = new Y(..) + * // .. when accessing the type use `y.define(..)` + * y.define('myArray', YArray).insert(..) + * y.define('myMap', YMap).set(..) + * + * @param {String} name + * @param {YType Constructor} TypeConstructor The constructor of the type definition + * @returns {YType} The created type + */ define (name, TypeConstructor) { let id = new RootID(name, TypeConstructor) let type = this.os.get(id) @@ -134,9 +216,23 @@ export default class Y extends NamedEventHandler { } return type } + + /** + * Get a defined type. The type must be defined locally. First define the + * type with {@link define}. + * + * This returns the same value as `y.share[name]` + * + * @param {String} name The typename + */ get (name) { return this.share[name] } + + /** + * Disconnect this Yjs Instance from the network. The connector will + * unsubscribe from the room and document updates are not shared anymore. + */ disconnect () { if (this.connected) { this.connected = false @@ -145,6 +241,10 @@ export default class Y extends NamedEventHandler { return Promise.resolve() } } + + /** + * If disconnected, tell the connector to reconnect to the room. + */ reconnect () { if (!this.connected) { this.connected = true @@ -153,6 +253,11 @@ export default class Y extends NamedEventHandler { return Promise.resolve() } } + + /** + * Disconnect from the room, and destroy all traces of this Yjs instance. + * Persisted data will remain until removed by the persistence adapter. + */ destroy () { super.destroy() this.share = null @@ -171,7 +276,9 @@ export default class Y extends NamedEventHandler { this.ds = null this.ss = null } + whenSynced () { + // TODO: remove this method return new Promise(resolve => { this.once('synced', () => { resolve() From bbc207aaa6826201ad4bf07ea2b75b22b77a1fb3 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 6 Mar 2018 03:17:36 +0100 Subject: [PATCH 07/22] restructer and move to esdoc --- documentation.yml | 43 ------------ examples/xml/index.html | 18 ++--- package.json | 4 +- rollup.browser.js | 6 +- rollup.node.js | 2 +- src/{Binding => Bindings}/Binding.js | 0 src/{Binding => Bindings}/DomBinding.js | 0 src/{Binding => Bindings}/QuillBinding.js | 0 src/{Binding => Bindings}/TextareaBinding.js | 0 src/Connector.js | 4 +- src/MessageHandler/binaryEncode.js | 2 +- src/MessageHandler/deleteSet.js | 2 +- src/MessageHandler/integrateRemoteStructs.js | 2 +- src/MessageHandler/messageToString.js | 6 +- src/MessageHandler/syncStep1.js | 6 +- src/Persistence.js | 4 +- src/Store/DeleteStore.js | 2 +- src/Store/OperationStore.js | 2 +- src/Store/StateStore.js | 2 +- src/Struct/Delete.js | 2 +- src/Struct/Item.js | 4 +- src/Struct/Type.js | 12 ++-- src/{Type => Types/YArray}/YArray.js | 0 src/{Type => Types/YMap}/YMap.js | 0 src/{Type => Types/YText}/YText.js | 0 .../y-xml/y-xml.js => Types/YXml/YXml.js} | 0 src/{Type/y-xml => Types/YXml}/YXmlElement.js | 14 ++-- src/{Type/y-xml => Types/YXml}/YXmlEvent.js | 0 .../y-xml => Types/YXml}/YXmlFragment.js | 5 +- src/{Type/y-xml => Types/YXml}/YXmlHook.js | 0 src/{Type/y-xml => Types/YXml}/YXmlText.js | 0 src/{Type/y-xml => Types/YXml}/domFilter.js | 0 src/{Type/y-xml => Types/YXml}/hooks.js | 0 src/{Type/y-xml => Types/YXml}/selection.js | 0 src/{Type/y-xml => Types/YXml}/utils.js | 2 +- src/{ => Util}/Binary/Decoder.js | 56 ++++++++------- src/{ => Util}/Binary/Encoder.js | 48 ++++++------- src/Util/{ => ID}/ID.js | 2 +- src/Util/{ => ID}/RootID.js | 0 src/Util/defragmentItemContent.js | 2 +- src/Util/simpleDiff.js | 30 +++++++- src/Util/structReferences.js | 36 +++++----- src/Util/writeJSONToType.js | 4 +- src/Y.dist.js | 51 ++++++++++++++ src/Y.js | 68 +++---------------- src/y-dist.cjs.js | 3 - test/encode-decode.tests.js | 4 +- test/red-black-tree.js | 2 +- 48 files changed, 223 insertions(+), 227 deletions(-) delete mode 100644 documentation.yml rename src/{Binding => Bindings}/Binding.js (100%) rename src/{Binding => Bindings}/DomBinding.js (100%) rename src/{Binding => Bindings}/QuillBinding.js (100%) rename src/{Binding => Bindings}/TextareaBinding.js (100%) rename src/{Type => Types/YArray}/YArray.js (100%) rename src/{Type => Types/YMap}/YMap.js (100%) rename src/{Type => Types/YText}/YText.js (100%) rename src/{Type/y-xml/y-xml.js => Types/YXml/YXml.js} (100%) rename src/{Type/y-xml => Types/YXml}/YXmlElement.js (94%) rename src/{Type/y-xml => Types/YXml}/YXmlEvent.js (100%) rename src/{Type/y-xml => Types/YXml}/YXmlFragment.js (98%) rename src/{Type/y-xml => Types/YXml}/YXmlHook.js (100%) rename src/{Type/y-xml => Types/YXml}/YXmlText.js (100%) rename src/{Type/y-xml => Types/YXml}/domFilter.js (100%) rename src/{Type/y-xml => Types/YXml}/hooks.js (100%) rename src/{Type/y-xml => Types/YXml}/selection.js (100%) rename src/{Type/y-xml => Types/YXml}/utils.js (99%) rename src/{ => Util}/Binary/Decoder.js (65%) rename src/{ => Util}/Binary/Encoder.js (70%) rename src/Util/{ => ID}/ID.js (89%) rename src/Util/{ => ID}/RootID.js (100%) create mode 100644 src/Y.dist.js delete mode 100644 src/y-dist.cjs.js diff --git a/documentation.yml b/documentation.yml deleted file mode 100644 index 925badf2..00000000 --- a/documentation.yml +++ /dev/null @@ -1,43 +0,0 @@ -toc: - - Y - - name: Shared Types - description: | - Types provide an abstraction over the shared data. - Shared types can be edited concurrently by peers. - - Type - - YArray - - YMap - - YText - - YXmlElement - - YXmlFragment - - YXmlHook - - YXmlText - - name: Bindings - description: | - A binding handles data binding from a Yjs type to - a data object - - Binding - - DomBinding - - QuillBinding - - TextareaBinding - - name: Events - description: | - Events describe changes on shared types. - - YArrayEvent - - YEvent - - YMapEvent - - YTextEvent - - YXmlEvent - - name: Binary Encoding - description: | - Yjs efficiently encodes the Yjs model to a binary format. - This section describes utility functions for binary encoding and decoding. - - BinaryEncoder - - BinaryDecoder - - toBinary - - fromBinary - - name: Relative Position - - RelativePosition - - fromRelativePosition - - getRelativePosition - - name: Utility diff --git a/examples/xml/index.html b/examples/xml/index.html index 366ed69a..78403a40 100644 --- a/examples/xml/index.html +++ b/examples/xml/index.html @@ -1,7 +1,7 @@ - + @@ -24,14 +24,16 @@ + diff --git a/test/y-xml.tests.js b/test/y-xml.tests.js index d168691c..6d77515c 100644 --- a/test/y-xml.tests.js +++ b/test/y-xml.tests.js @@ -14,7 +14,6 @@ test('events', async function xml1 (t) { var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) var event var remoteEvent - let expectedEvent xml0.observe(function (e) { delete e._content delete e.nodes @@ -30,21 +29,21 @@ test('events', async function xml1 (t) { xml0.setAttribute('key', 'value') t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key') await flushAll(t, users) - t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)') + 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') await flushAll(t, users) - t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)') + 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') await flushAll(t, users) - t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)') + t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)') // test childRemoved xml0.delete(0) t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element') await flushAll(t, users) - t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)') + t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)') await compareUsers(t, users) }) @@ -180,7 +179,7 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) { }) test('move element to a different position', async function xml13 (t) { - var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 }) + var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) dom0.append(document.createElement('div')) dom0.append(document.createElement('h1')) await flushAll(t, users) @@ -193,7 +192,7 @@ test('move element to a different position', async function xml13 (t) { }) test('filter node', async function xml14 (t) { - var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) + var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) let domFilter = (nodeName, attrs) => { if (nodeName === 'H1') { return null @@ -212,7 +211,7 @@ test('filter node', async function xml14 (t) { }) test('filter attribute', async function xml15 (t) { - var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) + var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) let domFilter = (nodeName, attrs) => { attrs.delete('hidden') return attrs @@ -231,7 +230,7 @@ test('filter attribute', async function xml15 (t) { }) test('deep element insert', async function xml16 (t) { - var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 }) + var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) let deepElement = document.createElement('p') let boldElement = document.createElement('b') let attrElement = document.createElement('img') diff --git a/tests-lib/test-connector.js b/tests-lib/test-connector.js index 8cb0fdb6..b83f2777 100644 --- a/tests-lib/test-connector.js +++ b/tests-lib/test-connector.js @@ -1,4 +1,3 @@ -/* global Y */ import { wait } from './helper' import { messageToString } from '../src/MessageHandler/messageToString' import AbstractConnector from '../src/Connector.js' From 32207cbca00eb914cbf3174e97fb5e54deb2623f Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 29 Mar 2018 16:36:34 +0200 Subject: [PATCH 15/22] implement new mark deleted / gc approach --- src/Store/DeleteStore.js | 146 ++++++++++++++------------------------ test/DeleteStore.tests.js | 83 ++++++++++++++++++++++ 2 files changed, 138 insertions(+), 91 deletions(-) create mode 100644 test/DeleteStore.tests.js diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js index 1aa62582..77639fd9 100644 --- a/src/Store/DeleteStore.js +++ b/src/Store/DeleteStore.js @@ -29,97 +29,61 @@ export default class DeleteStore extends Tree { var n = this.findWithUpperBound(id) return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len } - /* - * Mark an operation as deleted. returns the deleted node - */ + mark (id, length, gc) { + if (length === 0) return + // Step 1. Unmark range + const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1)) + // Resize left DSNode if necessary + if (leftD !== null && leftD._id.user === id.user) { + if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) { + // node is overlapping. need to resize + if (id.clock + length < leftD._id.clock + leftD.len) { + // overlaps new mark range and some more + // create another DSNode to the right of new mark + this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc)) + } + // resize left DSNode + leftD.len = id.clock - leftD._id.clock + } // Otherwise there is no overlapping + } + // Resize right DSNode if necessary + const upper = new ID(id.user, id.clock + length - 1) + const rightD = this.findWithUpperBound(upper) + if (rightD !== null && rightD._id.user === id.user) { + if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node + const d = id.clock + length - rightD._id.clock + rightD._id.clock += d + rightD.len -= d + } + } + // Now we only have to delete all inner marks + const deleteNodeIds = [] + this.iterate(id, upper, m => { + deleteNodeIds.push(m._id) + }) + for (let i = deleteNodeIds.length - 1; i >= 0; i--) { + this.delete(deleteNodeIds[i]) + } + let newMark = new DSNode(id, length, gc) + // Step 2. Check if we can extend left or right + if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) { + // We can extend left + leftD.len += length + newMark = leftD + } + const rightNext = this.find(new ID(id.user, id.clock + length)) + if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) { + // We can merge newMark and rightNext + newMark.len += rightNext.len + this.delete(rightNext._id) + } + if (leftD !== newMark) { + // only put if we didn't extend left + this.put(newMark) + } + } + // TODO: exchange markDeleted for mark() markDeleted (id, length) { - if (length == null) { - throw new Error('length must be defined') - } - var n = this.findWithUpperBound(id) - if (n != null && n._id.user === id.user) { - if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) { - // id is in n's range - var diff = id.clock + length - (n._id.clock + n.len) // overlapping right - if (diff > 0) { - // id+length overlaps n - if (!n.gc) { - n.len += diff - } else { - diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end) - if (diff < length) { - // a partial deletion - let nId = id.clone() - nId.clock += diff - n = new DSNode(nId, length - diff, false) - this.put(n) - } else { - // already gc'd - throw new Error( - 'DS reached an inconsistent state. Please report this issue!' - ) - } - } - } else { - // no overlapping, already deleted - return n - } - } else { - // cannot extend left (there is no left!) - n = new DSNode(id, length, false) - this.put(n) // TODO: you double-put !! - } - } else { - // cannot extend left - n = new DSNode(id, length, false) - this.put(n) - } - // can extend right? - var next = this.findNext(n._id) - if ( - next != null && - n._id.user === next._id.user && - n._id.clock + n.len >= next._id.clock - ) { - diff = n._id.clock + n.len - next._id.clock // from next.start to n.end - while (diff >= 0) { - // n overlaps with next - if (next.gc) { - // gc is stronger, so reduce length of n - n.len -= diff - if (diff >= next.len) { - // delete the missing range after next - diff = diff - next.len // missing range after next - if (diff > 0) { - this.put(n) // unneccessary? TODO! - this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff) - } - } - break - } else { - // we can extend n with next - if (diff > next.len) { - // n is even longer than next - // get next.next, and try to extend it - var _next = this.findNext(next._id) - this.delete(next._id) - if (_next == null || n._id.user !== _next._id.user) { - break - } else { - next = _next - diff = n._id.clock + n.len - next._id.clock // from next.start to n.end - // continue! - } - } else { - // n just partially overlaps with next. extend n, delete next, and break this loop - n.len += next.len - diff - this.delete(next._id) - break - } - } - } - } - this.put(n) - return n + this.mark(id, length, false) } } diff --git a/test/DeleteStore.tests.js b/test/DeleteStore.tests.js new file mode 100644 index 00000000..52e54382 --- /dev/null +++ b/test/DeleteStore.tests.js @@ -0,0 +1,83 @@ +import { test } from '../node_modules/cutest/cutest.mjs' +import simpleDiff from '../src/Util/simpleDiff.js' +import Chance from 'chance' +import DeleteStore from '../src/Store/DeleteStore.js' +import ID from '../src/Util/ID/ID.js' + +/** + * Converts a DS to an array of length 10. + * + * @example + * const ds = new DeleteStore() + * ds.mark(new ID(0, 0), 1, false) + * ds.mark(new ID(0, 1), 1, true) + * ds.mark(new ID(0, 3), 1, false) + * dsToArray(ds) // => [0, 1, undefined, 0] + * + * @return {Array<(undefined|number)>} Array of numbers indicating if array[i] is deleted (0), garbage collected (1), or undeleted (undefined). + */ +function dsToArray (ds) { + const array = [] + let i = 0 + ds.iterate(null, null, function (n) { + // fill with null + while (i < n._id.clock) { + array[i++] = null + } + while (i < n._id.clock + n.len) { + array[i++] = n.gc ? 1 : 0 + } + }) + return array +} + +test('DeleteStore', async function ds1 (t) { + const ds = new DeleteStore() + ds.mark(new ID(0, 1), 1, false) + ds.mark(new ID(0, 2), 1, false) + ds.mark(new ID(0, 3), 1, false) + t.compare(dsToArray(ds), [null, 0, 0, 0]) + ds.mark(new ID(0, 2), 1, true) + t.compare(dsToArray(ds), [null, 0, 1, 0]) + ds.mark(new ID(0, 1), 1, true) + t.compare(dsToArray(ds), [null, 1, 1, 0]) + ds.mark(new ID(0, 3), 1, true) + t.compare(dsToArray(ds), [null, 1, 1, 1]) + ds.mark(new ID(0, 5), 1, true) + ds.mark(new ID(0, 4), 1, true) + t.compare(dsToArray(ds), [null, 1, 1, 1, 1, 1]) + ds.mark(new ID(0, 0), 3, false) + t.compare(dsToArray(ds), [0, 0, 0, 1, 1, 1]) +}) + +test('random DeleteStore tests', async function randomDS (t) { + const chance = new Chance(t.getSeed() * 1000000000) + const ds = new DeleteStore() + const dsArray = [] + for (let i = 0; i < 200; i++) { + const pos = chance.integer({ min: 0, max: 10 }) + const len = chance.integer({ min: 0, max: 4 }) + const gc = chance.bool() + ds.mark(new ID(0, pos), len, gc) + for (let j = 0; j < len; j++) { + dsArray[pos + j] = gc ? 1 : 0 + } + } + // fill empty fields + for (let i = 0; i < dsArray.length; i++) { + if (dsArray[i] !== 0 && dsArray[i] !== 1) { + dsArray[i] = null + } + } + t.compare(dsToArray(ds), dsArray, 'expected DS result') + let size = 0 + let lastEl = null + for (let i = 0; i < dsArray.length; i++) { + let el = dsArray[i] + if (lastEl !== el && el !== null) { + size++ + } + lastEl = el + } + t.compare(size, ds.length, 'expected ds size') +}) From d915c8dd13e81204cea098dd9a93611195626336 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 29 Mar 2018 18:24:14 +0200 Subject: [PATCH 16/22] prelim gc --- src/Struct/Delete.js | 6 ++--- src/Struct/GC.js | 44 ++++++++++++++++++++++++++++++++++ src/Struct/Item.js | 29 +++++++++++++++++++++- src/Struct/Type.js | 33 +++++++++++++++++++++---- src/Types/YXml/YXmlFragment.js | 4 ++-- src/Types/YXml/YXmlText.js | 4 ++-- src/Util/structReferences.js | 3 +++ 7 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/Struct/GC.js diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js index 16b0bd0d..8e1e7c21 100644 --- a/src/Struct/Delete.js +++ b/src/Struct/Delete.js @@ -13,18 +13,18 @@ export function deleteItemRange (y, user, clock, range) { if (item !== null) { if (!item._deleted) { item._splitAt(y, range) - item._delete(y, createDelete) + item._delete(y, createDelete, true) } let itemLen = item._length range -= itemLen clock += itemLen if (range > 0) { let node = y.os.findNode(new ID(user, clock)) - while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { + while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { const nodeVal = node.val if (!nodeVal._deleted) { nodeVal._splitAt(y, range) - nodeVal._delete(y, createDelete) + nodeVal._delete(y, createDelete, true) } const nodeLen = nodeVal._length range -= nodeLen diff --git a/src/Struct/GC.js b/src/Struct/GC.js new file mode 100644 index 00000000..d2cc7f02 --- /dev/null +++ b/src/Struct/GC.js @@ -0,0 +1,44 @@ + +export default class GC { + constructor () { + this._id = null + this._length = 0 + } + + get _deleted () { + return true + } + + integrate () { + } + + /** + * 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 {BinaryEncoder} encoder The encoder to write data to. + * @private + */ + _toBinary (encoder) { + encoder.writeUint8(getStructReference(this.constructor)) + encoder.writeID(this._id) + encoder.writeVarUint(this._length) + } + + /** + * Read the next Item in a Decoder and fill this Item with the read data. + * + * This is called when data is received from a remote peer. + * + * @param {Y} y The Yjs instance that this Item belongs to. + * @param {BinaryDecoder} decoder The decoder object to read data from. + * @private + */ + _fromBinary (y, decoder) { + this._id = decoder.readID() + this._length = decoder.readVarUint() + return [] + } +} \ No newline at end of file diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 79f97f3c..657fc97f 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -3,6 +3,7 @@ import ID from '../Util/ID/ID.js' import { RootFakeUserID } from '../Util/ID/RootID.js' import Delete from './Delete.js' import { transactionTypeChanged } from '../Transaction.js' +import GC from './GC.js' /** * @private @@ -220,7 +221,7 @@ export default class Item { _delete (y, createDelete = true) { if (!this._deleted) { this._deleted = true - y.ds.markDeleted(this._id, this._length) + y.ds.mark(this._id, this._length, false) let del = new Delete() del._targetID = this._id del._length = this._length @@ -236,6 +237,32 @@ export default class Item { } } + _gcChildren (y) {} + + _gc (y) { + y.ds.mark(this._id, this._length, true) + const gc = new GC() + gc._id = this._id + gc._length = this._length + y.os.delete(this._id) + let n = y.os.put(gc) + const prev = n.prev().val + if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { + // TODO: do merging for all items! + prev._length += n.val._length + y.os.delete(n.val._id) + n = prev + } + if (n.val) { + n = n.val + } + const next = y.os.findNext(n._id) + if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { + n._length += next._length + y.os.delete(n._id) + } + } + /** * This is called right before this Item receives any children. * It can be overwritten to apply pending changes before applying remote changes diff --git a/src/Struct/Type.js b/src/Struct/Type.js index cb98f374..2d96dcc2 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -30,6 +30,14 @@ export function getListItemIDByPosition (type, i) { } } +function gcChildren (y, item) { + while (item !== null) { + item._delete(y, false, true) + item._gc(y) + item = item._right + } +} + /** * Abstract Yjs Type class */ @@ -184,6 +192,20 @@ export default class Type extends Item { } } + _gcChildren (y) { + gcChildren(y, this._start) + this._start = null + this._map.forEach(item => { + gcChildren(y, item) + }) + this._map = new Map() + } + + _gc (y) { + this._gcChildren(y) + super._gc(y) + } + /** * @private * Mark this Item as deleted. @@ -192,22 +214,25 @@ export default class Type extends Item { * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren = true) { + super._delete(y, createDelete, gcChildren) y._transaction.changedTypes.delete(this) // delete map types for (let value of this._map.values()) { if (value instanceof Item && !value._deleted) { - value._delete(y, false) + value._delete(y, false, gcChildren) } } // delete array types let t = this._start while (t !== null) { if (!t._deleted) { - t._delete(y, false) + t._delete(y, false, gcChildren) } t = t._right } + if (gcChildren) { + this._gcChildren(y) + } } } diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index babb1638..8bbe932c 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -126,8 +126,8 @@ export default class YXmlFragment extends YArray { * * @private */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren) { + super._delete(y, createDelete, gcChildren) } /** diff --git a/src/Types/YXml/YXmlText.js b/src/Types/YXml/YXmlText.js index 848fe985..81c9ee69 100644 --- a/src/Types/YXml/YXmlText.js +++ b/src/Types/YXml/YXmlText.js @@ -36,7 +36,7 @@ export default class YXmlText extends YText { * * @private */ - _delete (y, createDelete) { - super._delete(y, createDelete) + _delete (y, createDelete, gcChildren) { + super._delete(y, createDelete, gcChildren) } } diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index 4101e455..a79d8c71 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -8,6 +8,7 @@ import ItemJSON from '../Struct/ItemJSON.js' import ItemString from '../Struct/ItemString.js' import ItemFormat from '../Struct/ItemFormat.js' import ItemEmbed from '../Struct/ItemEmbed.js' +import GC from '../Struct/GC.js' const structs = new Map() const references = new Map() @@ -54,3 +55,5 @@ registerStruct(6, YXmlFragment) registerStruct(7, YXmlElement) registerStruct(8, YXmlText) registerStruct(9, YXmlHook) + +registerStruct(12, GC) \ No newline at end of file From ef6eb08335a352b3e023eedffc87217ed3e1b6a2 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 19 Apr 2018 18:28:25 +0200 Subject: [PATCH 17/22] fix most gc bugs - test suite running again --- src/MessageHandler/integrateRemoteStructs.js | 10 +++- src/Store/DeleteStore.js | 2 +- src/Store/OperationStore.js | 31 +++++++----- src/Struct/GC.js | 52 ++++++++++++++++++-- src/Struct/Item.js | 32 +++++------- tests-lib/helper.js | 26 +++++++--- 6 files changed, 109 insertions(+), 44 deletions(-) diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index 3abe9d8f..d2067f3c 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -1,6 +1,7 @@ import { getStruct } from '../Util/structReferences.js' import BinaryDecoder from '../Util/Binary/Decoder.js' import { logID } from './messageToString.js' +import GC from '../Struct/GC.js'; class MissingEntry { constructor (decoder, missing, struct) { @@ -24,7 +25,14 @@ function _integrateRemoteStructHelper (y, struct) { if (y.ss.getState(id.user) > id.clock) { return } - struct._integrate(y) + if (struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) { + // Is either a GC or Item with an undeleted parent + // save to integrate + struct._integrate(y) + } else { + // Is an Item. parent was deleted. + struct._gc(y) + } let msu = y._missingStructs.get(id.user) if (msu != null) { let clock = id.clock diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js index 77639fd9..eb5b1a9e 100644 --- a/src/Store/DeleteStore.js +++ b/src/Store/DeleteStore.js @@ -52,7 +52,7 @@ export default class DeleteStore extends Tree { if (rightD !== null && rightD._id.user === id.user) { if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node const d = id.clock + length - rightD._id.clock - rightD._id.clock += d + rightD._id = new ID(rightD._id.user, rightD._id.clock + d) rightD.len -= d } } diff --git a/src/Store/OperationStore.js b/src/Store/OperationStore.js index fdc74b03..6ee790a6 100644 --- a/src/Store/OperationStore.js +++ b/src/Store/OperationStore.js @@ -2,6 +2,7 @@ import Tree from '../Util/Tree.js' import RootID from '../Util/ID/RootID.js' import { getStruct } from '../Util/structReferences.js' import { logID } from '../MessageHandler/messageToString.js' +import GC from '../Struct/GC.js' export default class OperationStore extends Tree { constructor (y) { @@ -11,17 +12,25 @@ export default class OperationStore extends Tree { logTable () { const items = [] this.iterate(null, null, function (item) { - items.push({ - id: logID(item), - origin: logID(item._origin === null ? null : item._origin._lastId), - left: logID(item._left === null ? null : item._left._lastId), - right: logID(item._right), - right_origin: logID(item._right_origin), - parent: logID(item._parent), - parentSub: item._parentSub, - deleted: item._deleted, - content: JSON.stringify(item._content) - }) + if (item.constructor === GC) { + items.push({ + id: logID(item), + content: item._length, + deleted: 'GC' + }) + } else { + items.push({ + id: logID(item), + origin: logID(item._origin === null ? null : item._origin._lastId), + left: logID(item._left === null ? null : item._left._lastId), + right: logID(item._right), + right_origin: logID(item._right_origin), + parent: logID(item._parent), + parentSub: item._parentSub, + deleted: item._deleted, + content: JSON.stringify(item._content) + }) + } }) console.table(items) } diff --git a/src/Struct/GC.js b/src/Struct/GC.js index d2cc7f02..1a90c888 100644 --- a/src/Struct/GC.js +++ b/src/Struct/GC.js @@ -1,4 +1,8 @@ +import { getStructReference } from '../Util/structReferences.js' +import { RootFakeUserID } from '../Util/ID/RootID.js' +import ID from '../Util/ID/ID.js' +// TODO should have the same base class as Item export default class GC { constructor () { this._id = null @@ -9,7 +13,38 @@ export default class GC { return true } - integrate () { + _integrate (y) { + const id = this._id + const userState = y.ss.getState(id.user) + if (id.clock === userState) { + y.ss.setState(id.user, id.clock + this._length) + } + y.ds.mark(this._id, this._length, true) + let n = y.os.put(this) + const prev = n.prev().val + if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { + // TODO: do merging for all items! + prev._length += n.val._length + y.os.delete(n.val._id) + n = prev + } + if (n.val) { + n = n.val + } + const next = y.os.findNext(n._id) + if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { + n._length += next._length + y.os.delete(next._id) + } + + if (id.user !== RootFakeUserID) { + if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) { + y.connector.broadcastStruct(this) + } + if (y.persistence !== null) { + y.persistence.saveStruct(y, this) + } + } } /** @@ -37,8 +72,17 @@ export default class GC { * @private */ _fromBinary (y, decoder) { - this._id = decoder.readID() + const id = decoder.readID() + this._id = id this._length = decoder.readVarUint() - return [] - } + const missing = [] + if (y.ss.getState(id.user) < id.clock) { + missing.push(new ID(id.user, id.clock - 1)) + } + return missing + } + + _splitAt () { + return this + } } \ No newline at end of file diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 657fc97f..9b76e14b 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -240,27 +240,11 @@ export default class Item { _gcChildren (y) {} _gc (y) { - y.ds.mark(this._id, this._length, true) const gc = new GC() gc._id = this._id gc._length = this._length y.os.delete(this._id) - let n = y.os.put(gc) - const prev = n.prev().val - if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { - // TODO: do merging for all items! - prev._length += n.val._length - y.os.delete(n.val._id) - n = prev - } - if (n.val) { - n = n.val - } - const next = y.os.findNext(n._id) - if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { - n._length += next._length - y.os.delete(n._id) - } + gc._integrate(y) } /** @@ -511,9 +495,19 @@ export default class Item { } } else if (this._parent === null) { if (this._origin !== null) { - this._parent = this._origin._parent + if (this._origin.constructor === GC) { + // if origin is a gc, set parent also gc'd + this._parent = this._origin + } else { + this._parent = this._origin._parent + } } else if (this._right_origin !== null) { - this._parent = this._right_origin._parent + // if origin is a gc, set parent also gc'd + if (this._right_origin.constructor === GC) { + this._parent = this._right_origin + } else { + this._parent = this._right_origin._parent + } } } if (info & 0b1000) { diff --git a/tests-lib/helper.js b/tests-lib/helper.js index 26a50f08..a8425a9d 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -8,6 +8,7 @@ import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' import Quill from 'quill' +import GC from '../src/Struct/GC.js'; export const Y = _Y @@ -110,13 +111,22 @@ export async function compareUsers (t, users) { var data = {} let ops = [] u.os.iterate(null, null, function (op) { - const json = { - id: op._id, - left: op._left === null ? null : op._left._lastId, - right: op._right === null ? null : op._right._id, - length: op._length, - deleted: op._deleted, - parent: op._parent._id + let json + if (op.constructor === GC) { + json = { + type: 'GC', + id: op._id, + length: op._length + } + } else { + json = { + id: op._id, + left: op._left === null ? null : op._left._lastId, + right: op._right === null ? null : op._right._id, + length: op._length, + deleted: op._deleted, + parent: op._parent._id + } } if (op instanceof ItemJSON || op instanceof ItemString) { json.content = op._content @@ -186,7 +196,7 @@ export async function initArrays (t, opts) { y.dom = dom y.on('afterTransaction', function () { for (let missing of y._missingStructs.values()) { - if (Array.from(missing.values()).length > 0) { + if (missing.size > 0) { console.error(new Error('Test check in "afterTransaction": missing should be empty!')) } } From 94933a704d3761c7e33115213503c85a0ff4b969 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 23 Apr 2018 13:25:30 +0200 Subject: [PATCH 18/22] correctly handle gc with UndoManager and un-merge when syncing --- rollup.browser.js | 4 +--- src/MessageHandler/deleteSet.js | 8 ++++---- src/MessageHandler/syncStep1.js | 10 +++++++++- src/Store/DeleteStore.js | 1 + src/Struct/Delete.js | 6 +++--- src/Struct/GC.js | 8 +++++++- src/Struct/Item.js | 9 +++++++-- src/Struct/Type.js | 5 ++++- src/Util/UndoManager.js | 1 + src/Y.js | 1 + 10 files changed, 38 insertions(+), 15 deletions(-) diff --git a/rollup.browser.js b/rollup.browser.js index a0b87065..d5f6c137 100644 --- a/rollup.browser.js +++ b/rollup.browser.js @@ -19,8 +19,7 @@ export default { browser: true }), commonjs(), - // babel(), - /* + babel(), uglify({ mangle: { except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item'] @@ -36,7 +35,6 @@ export default { } } }) - */ ], banner: ` /** diff --git a/src/MessageHandler/deleteSet.js b/src/MessageHandler/deleteSet.js index cb4b1c3b..2b54abfc 100644 --- a/src/MessageHandler/deleteSet.js +++ b/src/MessageHandler/deleteSet.js @@ -92,7 +92,7 @@ export function readDeleteSet (y, decoder) { // delete maximum the len of d // else delete as much as possible diff = Math.min(n._id.clock - d[0], d[1]) - // deleteItemRange(y, user, d[0], diff) + // deleteItemRange(y, user, d[0], diff, true) deletions.push([user, d[0], diff]) } else { // 3) @@ -100,7 +100,7 @@ export function readDeleteSet (y, decoder) { if (d[2] && !n.gc) { // d marks as gc'd but n does not // then delete either way - // deleteItemRange(y, user, d[0], Math.min(diff, d[1])) + // deleteItemRange(y, user, d[0], Math.min(diff, d[1]), true) deletions.push([user, d[0], Math.min(diff, d[1])]) } } @@ -117,12 +117,12 @@ export function readDeleteSet (y, decoder) { // Adapt the Tree implementation to support delete while iterating for (let i = deletions.length - 1; i >= 0; i--) { const del = deletions[i] - deleteItemRange(y, del[0], del[1], del[2]) + deleteItemRange(y, del[0], del[1], del[2], true) } // for the rest.. just apply it for (; pos < dv.length; pos++) { d = dv[pos] - deleteItemRange(y, user, d[0], d[1]) + deleteItemRange(y, user, d[0], d[1], true) // deletions.push([user, d[0], d[1], d[2]]) } } diff --git a/src/MessageHandler/syncStep1.js b/src/MessageHandler/syncStep1.js index 8834d9f0..046177c3 100644 --- a/src/MessageHandler/syncStep1.js +++ b/src/MessageHandler/syncStep1.js @@ -42,7 +42,15 @@ export function writeStructs (y, encoder, ss) { for (let user of y.ss.state.keys()) { let clock = ss.get(user) || 0 if (user !== RootFakeUserID) { - y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) { + const minBound = new ID(user, clock) + const overlappingLeft = y.os.findPrev(minBound) + const rightID = overlappingLeft === null ? null : overlappingLeft._id + if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) { + const struct = overlappingLeft._clonePartial(clock - rightID.clock) + struct._toBinary(encoder) + len++ + } + y.os.iterate(minBound, new ID(user, Number.MAX_VALUE), function (struct) { struct._toBinary(encoder) len++ }) diff --git a/src/Store/DeleteStore.js b/src/Store/DeleteStore.js index eb5b1a9e..f35bd928 100644 --- a/src/Store/DeleteStore.js +++ b/src/Store/DeleteStore.js @@ -1,3 +1,4 @@ + import Tree from '../Util/Tree.js' import ID from '../Util/ID/ID.js' diff --git a/src/Struct/Delete.js b/src/Struct/Delete.js index 8e1e7c21..74bdbeb1 100644 --- a/src/Struct/Delete.js +++ b/src/Struct/Delete.js @@ -7,7 +7,7 @@ import { logID } from '../MessageHandler/messageToString.js' * Delete all items in an ID-range * TODO: implement getItemCleanStartNode for better performance (only one lookup) */ -export function deleteItemRange (y, user, clock, range) { +export function deleteItemRange (y, user, clock, range, gcChildren) { const createDelete = y.connector !== null && y.connector._forwardAppliedStructs let item = y.os.getItemCleanStart(new ID(user, clock)) if (item !== null) { @@ -24,7 +24,7 @@ export function deleteItemRange (y, user, clock, range) { const nodeVal = node.val if (!nodeVal._deleted) { nodeVal._splitAt(y, range) - nodeVal._delete(y, createDelete, true) + nodeVal._delete(y, createDelete, gcChildren) } const nodeLen = nodeVal._length range -= nodeLen @@ -100,7 +100,7 @@ export default class Delete { if (!locallyCreated) { // from remote const id = this._targetID - deleteItemRange(y, id.user, id.clock, this._length) + deleteItemRange(y, id.user, id.clock, this._length, false) } else if (y.connector !== null) { // from local y.connector.broadcastStruct(this) diff --git a/src/Struct/GC.js b/src/Struct/GC.js index 1a90c888..a23d55b6 100644 --- a/src/Struct/GC.js +++ b/src/Struct/GC.js @@ -36,7 +36,6 @@ export default class GC { n._length += next._length y.os.delete(next._id) } - if (id.user !== RootFakeUserID) { if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) { y.connector.broadcastStruct(this) @@ -85,4 +84,11 @@ export default class GC { _splitAt () { return this } + + _clonePartial (diff) { + const gc = new GC() + gc._id = new ID(this._id.user, this._id.clock + diff) + gc._length = this._length - diff + return gc + } } \ No newline at end of file diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 9b76e14b..1ff928f2 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -1,6 +1,6 @@ import { getStructReference } from '../Util/structReferences.js' import ID from '../Util/ID/ID.js' -import { RootFakeUserID } from '../Util/ID/RootID.js' +import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js' import Delete from './Delete.js' import { transactionTypeChanged } from '../Transaction.js' import GC from './GC.js' @@ -486,7 +486,12 @@ export default class Item { const parentID = decoder.readID() // parent does not change, so we don't have to search for it again if (this._parent === null) { - const parent = y.os.get(parentID) + let parent + if (parentID.constructor === RootID) { + parent = y.os.get(parentID) + } else { + parent = y.os.getItem(parentID) + } if (parent === null) { missing.push(parentID) } else { diff --git a/src/Struct/Type.js b/src/Struct/Type.js index 2d96dcc2..44f075ad 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -214,7 +214,10 @@ export default class Type extends Item { * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. */ - _delete (y, createDelete, gcChildren = true) { + _delete (y, createDelete, gcChildren) { + if (gcChildren === undefined) { + gcChildren = y._hasUndoManager === false + } super._delete(y, createDelete, gcChildren) y._transaction.changedTypes.delete(this) // delete map types diff --git a/src/Util/UndoManager.js b/src/Util/UndoManager.js index 54adbbde..0810ffc8 100644 --- a/src/Util/UndoManager.js +++ b/src/Util/UndoManager.js @@ -75,6 +75,7 @@ export default class UndoManager { this._lastTransactionWasUndo = false const y = scope._y this.y = y + y._hasUndoManager = true y.on('afterTransaction', (y, transaction, remote) => { if (!remote && transaction.changedParentTypes.has(scope)) { let reverseOperation = new ReverseOperation(y, transaction) diff --git a/src/Y.js b/src/Y.js index ca7b63ef..7a1ab318 100644 --- a/src/Y.js +++ b/src/Y.js @@ -79,6 +79,7 @@ export default class Y extends NamedEventHandler { } // for compatibility with isParentOf this._parent = null + this._hasUndoManager = false } _setContentReady () { if (!this._contentReady) { From 1fe37c565e34f9e0fa35128e906bc844488f3f8b Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 26 Apr 2018 13:26:21 +0200 Subject: [PATCH 19/22] hooks port to domBinding --- examples/html-editor-drawing-hook/index.js | 26 +++++++++--------- src/Bindings/DomBinding/DomBinding.js | 7 +++-- src/Bindings/DomBinding/domToType.js | 31 ++++++++++++++++------ src/Bindings/DomBinding/typeObserver.js | 5 ++-- src/Types/YXml/YXmlElement.js | 6 +++-- src/Types/YXml/YXmlFragment.js | 8 +++--- src/Types/YXml/YXmlHook.js | 18 ++++++++----- src/Types/YXml/YXmlText.js | 4 ++- src/Types/YXml/hooks.js | 14 ---------- 9 files changed, 68 insertions(+), 51 deletions(-) delete mode 100644 src/Types/YXml/hooks.js diff --git a/examples/html-editor-drawing-hook/index.js b/examples/html-editor-drawing-hook/index.js index 27b58a10..dca6a6d3 100644 --- a/examples/html-editor-drawing-hook/index.js +++ b/examples/html-editor-drawing-hook/index.js @@ -1,7 +1,20 @@ /* global Y, d3 */ +const hooks = { + "magic-drawing": { + fillType: function (dom, type) { + initDrawingBindings(type, dom) + }, + createDom: function (type) { + const dom = document.createElement('magic-drawing') + initDrawingBindings(type, dom) + return dom + } + } +} + window.onload = function () { - window.yXmlType.bindToDom(document.body) + window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks }) } window.addMagicDrawing = function addMagicDrawing () { @@ -96,17 +109,6 @@ function initDrawingBindings (type, dom) { } } -Y.XmlHook.addHook('magic-drawing', { - fillType: function (dom, type) { - initDrawingBindings(type, dom) - }, - createDom: function (type) { - const dom = document.createElement('magic-drawing') - initDrawingBindings(type, dom) - return dom - } -}) - let y = new Y('html-editor-drawing-hook-example', { connector: { name: 'websockets-client', diff --git a/src/Bindings/DomBinding/DomBinding.js b/src/Bindings/DomBinding/DomBinding.js index a2d10bf1..ae5a2894 100644 --- a/src/Bindings/DomBinding/DomBinding.js +++ b/src/Bindings/DomBinding/DomBinding.js @@ -30,6 +30,9 @@ export default class DomBinding extends Binding { constructor (type, target, opts = {}) { // Binding handles textType as this.type and domTextarea as this.target super(type, target) + this.opts = opts + opts.document = opts.document || document + opts.hooks = opts.hooks || {} /** * Maps each DOM element to the type that it is associated with. * @type {Map} @@ -49,11 +52,11 @@ export default class DomBinding extends Binding { // set initial value target.innerHTML = '' for (let child of type) { - target.insertBefore(child.toDom(this.domToType, this.typeToDom), null) + target.insertBefore(child.toDom(opts.document, opts.hooks, this), null) } this._typeObserver = typeObserver.bind(this) this._domObserver = (mutations) => { - domObserver.call(this, mutations, opts._document) + domObserver.call(this, mutations, opts.document) } type.observeDeep(this._typeObserver) this._mutationObserver = new MutationObserver(this._domObserver) diff --git a/src/Bindings/DomBinding/domToType.js b/src/Bindings/DomBinding/domToType.js index d95f8d28..fdb939e5 100644 --- a/src/Bindings/DomBinding/domToType.js +++ b/src/Bindings/DomBinding/domToType.js @@ -1,5 +1,5 @@ -import { YXmlText, YXmlElement } from '../../Types/YXml/YXml.js' +import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' import { createAssociation } from './util.js' /** @@ -15,14 +15,29 @@ export default function domToType (element, _document = document, binding) { let type switch (element.nodeType) { case _document.ELEMENT_NODE: - type = new YXmlElement(element.nodeName) - const attrs = element.attributes - for (let i = attrs.length - 1; i >= 0; i--) { - const attr = attrs[i] - type.setAttribute(attr.name, attr.value) + let hookName = element.dataset.yjsHook + let hook + if (hookName !== undefined) { + hook = binding.opts.hooks[hookName] + if (hook === undefined) { + console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`) + delete element.dataset.yjsHook + hookName = undefined + } + } + if (hookName === undefined) { + type = new YXmlElement(element.nodeName) + const attrs = element.attributes + for (let i = attrs.length - 1; i >= 0; i--) { + const attr = attrs[i] + type.setAttribute(attr.name, attr.value) + } + const children = Array.from(element.childNodes).map(e => domToType(e, _document, binding)) + type.insert(0, children) + } else { + type = new YXmlHook(hookName) + hook.fillType(element, type) } - const children = Array.from(element.childNodes).map(e => domToType(e, _document, binding)) - type.insert(0, children) break case _document.TEXT_NODE: type = new YXmlText() diff --git a/src/Bindings/DomBinding/typeObserver.js b/src/Bindings/DomBinding/typeObserver.js index e5b4343a..1417cc4e 100644 --- a/src/Bindings/DomBinding/typeObserver.js +++ b/src/Bindings/DomBinding/typeObserver.js @@ -6,7 +6,7 @@ import { removeDomChildrenUntilElementFound } from './util.js' /** * @private */ -export default function typeObserver (events, _document) { +export default function typeObserver (events) { this._mutualExclude(() => { events.forEach(event => { const yxml = event.target @@ -37,11 +37,10 @@ export default function typeObserver (events, _document) { let currentChild = dom.firstChild yxml.forEach(childType => { const childNode = this.typeToDom.get(childType) - const binding = this switch (childNode) { case undefined: // Does not exist. Create it. - const node = childType.toDom(_document, binding) + const node = childType.toDom(this.opts.document, this.opts.hooks, this) dom.insertBefore(node, currentChild) break case false: diff --git a/src/Types/YXml/YXmlElement.js b/src/Types/YXml/YXmlElement.js index 0e5463e5..43f73e13 100644 --- a/src/Types/YXml/YXmlElement.js +++ b/src/Types/YXml/YXmlElement.js @@ -164,6 +164,8 @@ export default class YXmlElement extends YXmlFragment { * @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 {DomBinding} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. @@ -171,14 +173,14 @@ export default class YXmlElement extends YXmlFragment { * * @public */ - toDom (_document = document, binding) { + toDom (_document = document, hooks = {}, binding) { const dom = _document.createElement(this.nodeName) let attrs = this.getAttributes() for (let key in attrs) { dom.setAttribute(key, attrs[key]) } this.forEach(yxml => { - dom.appendChild(yxml.toDom(_document, binding)) + dom.appendChild(yxml.toDom(_document, hooks, binding)) }) createAssociation(binding, dom, this) return dom diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index 8bbe932c..827356a0 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -136,18 +136,20 @@ export default class YXmlFragment extends YArray { * @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 {DomBinding} [binding] You should not set this property. This is * used if DomBinding wants to create a - * association to the created DOM type. + * 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, binding) { + toDom (_document = document, hooks = {}, binding) { const fragment = _document.createDocumentFragment() createAssociation(binding, fragment, this) this.forEach(xmlType => { - fragment.insertBefore(xmlType.toDom(_document, binding), null) + fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null) }) return fragment } diff --git a/src/Types/YXml/YXmlHook.js b/src/Types/YXml/YXmlHook.js index d840ab11..ec855f8e 100644 --- a/src/Types/YXml/YXmlHook.js +++ b/src/Types/YXml/YXmlHook.js @@ -1,5 +1,4 @@ import YMap from '../YMap/YMap.js' -import { getHook, addHook } from './hooks.js' /** * You can manage binding to a custom type with YXmlHook. @@ -35,16 +34,24 @@ export default class YXmlHook extends YMap { * @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 {DomBinding} [binding] You should not set this property. This is * used if DomBinding wants to create a - * association to the created DOM type. + * 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) { - const dom = getHook(this.hookName).createDom(this) - dom._yjsHook = this.hookName + 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.dataset.yjsHook = this.hookName return dom } @@ -97,4 +104,3 @@ export default class YXmlHook extends YMap { super._integrate(y) } } -YXmlHook.addHook = addHook diff --git a/src/Types/YXml/YXmlText.js b/src/Types/YXml/YXmlText.js index 81c9ee69..c0339806 100644 --- a/src/Types/YXml/YXmlText.js +++ b/src/Types/YXml/YXmlText.js @@ -14,6 +14,8 @@ export default class YXmlText extends YText { * @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 {DomBinding} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. @@ -21,7 +23,7 @@ export default class YXmlText extends YText { * * @public */ - toDom (_document = document, binding) { + toDom (_document = document, hooks, binding) { const dom = _document.createTextNode(this.toString()) createAssociation(binding, dom, this) return dom diff --git a/src/Types/YXml/hooks.js b/src/Types/YXml/hooks.js deleted file mode 100644 index 18e5de5c..00000000 --- a/src/Types/YXml/hooks.js +++ /dev/null @@ -1,14 +0,0 @@ - -const xmlHooks = {} - -export function addHook (name, hook) { - xmlHooks[name] = hook -} - -export function getHook (name) { - const hook = xmlHooks[name] - if (hook === undefined) { - throw new Error(`The hook "${name}" is not specified! You must not access this hook!`) - } - return hook -} From e788ad13334c26cad7b1c9c5e85b486e891e91e8 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 26 Apr 2018 14:07:12 +0200 Subject: [PATCH 20/22] fix hook binding --- src/Types/YXml/YXmlHook.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Types/YXml/YXmlHook.js b/src/Types/YXml/YXmlHook.js index ec855f8e..445f107b 100644 --- a/src/Types/YXml/YXmlHook.js +++ b/src/Types/YXml/YXmlHook.js @@ -1,4 +1,5 @@ import YMap from '../YMap/YMap.js' +import { createAssociation } from '../../Bindings/DomBinding/util.js' /** * You can manage binding to a custom type with YXmlHook. @@ -52,6 +53,7 @@ export default class YXmlHook extends YMap { dom = document.createElement(this.hookName) } dom.dataset.yjsHook = this.hookName + createAssociation(binding, dom, this) return dom } From 99f92cb9a048e1c549aab89ebc36e2c517dffbef Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 26 Apr 2018 16:01:17 +0200 Subject: [PATCH 21/22] fix filtering --- src/Bindings/DomBinding/domToType.js | 27 ++++++++++++++++++++------- src/Bindings/DomBinding/filter.js | 12 ++++++++++++ src/Bindings/DomBinding/util.js | 9 ++++++++- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Bindings/DomBinding/domToType.js b/src/Bindings/DomBinding/domToType.js index fdb939e5..b906b53b 100644 --- a/src/Bindings/DomBinding/domToType.js +++ b/src/Bindings/DomBinding/domToType.js @@ -1,6 +1,7 @@ import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' import { createAssociation } from './util.js' +import { filterDomAttributes } from './filter.js' /** * Creates a Yjs type (YXml) based on the contents of a DOM Element. @@ -17,6 +18,7 @@ export default function domToType (element, _document = document, binding) { case _document.ELEMENT_NODE: let hookName = element.dataset.yjsHook let hook + // configure `hookName !== undefined` if element is a hook. if (hookName !== undefined) { hook = binding.opts.hooks[hookName] if (hook === undefined) { @@ -26,15 +28,26 @@ export default function domToType (element, _document = document, binding) { } } if (hookName === undefined) { - type = new YXmlElement(element.nodeName) - const attrs = element.attributes - for (let i = attrs.length - 1; i >= 0; i--) { - const attr = attrs[i] - type.setAttribute(attr.name, attr.value) + // Not a hook + const attrs = filterDomAttributes(element, binding.filter) + if (attrs === null) { + type = false + } else { + type = new YXmlElement(element.nodeName) + attrs.forEach((val, key) => { + type.setAttribute(key, val) + }) + const children = [] + for (let elem of element.childNodes) { + const type = domToType(elem, _document, binding) + if (type !== false) { + children.push(type) + } + } + type.insert(0, children) } - const children = Array.from(element.childNodes).map(e => domToType(e, _document, binding)) - type.insert(0, children) } else { + // Is a hook type = new YXmlHook(hookName) hook.fillType(element, type) } diff --git a/src/Bindings/DomBinding/filter.js b/src/Bindings/DomBinding/filter.js index 81fac728..4fea3f84 100644 --- a/src/Bindings/DomBinding/filter.js +++ b/src/Bindings/DomBinding/filter.js @@ -13,6 +13,18 @@ export function defaultFilter (nodeName, attrs) { return attrs } +/** + * + */ +export function filterDomAttributes (dom, filter) { + const attrs = new Map() + for (let i = dom.attributes.length - 1; i >= 0; i--) { + const attr = dom.attributes[i] + attrs.set(attr.name, attr.value) + } + return filter(dom.nodeName, attrs) +} + /** * Applies a filter on a type. * diff --git a/src/Bindings/DomBinding/util.js b/src/Bindings/DomBinding/util.js index 259a4f31..e3f0ed0e 100644 --- a/src/Bindings/DomBinding/util.js +++ b/src/Bindings/DomBinding/util.js @@ -54,7 +54,14 @@ export function createAssociation (domBinding, dom, type) { * @private */ export function insertDomElementsAfter (type, prev, doms, _document, binding) { - return type.insertAfter(prev, doms.map(dom => domToType(dom, _document, binding))) + const types = [] + for (let dom of doms) { + const t = domToType(dom, _document, binding) + if (t !== false) { + types.push(t) + } + } + return type.insertAfter(prev, types) } /** From a54d826d6d03df9c76bc1eadd636adf71736b383 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 27 Apr 2018 18:33:28 +0200 Subject: [PATCH 22/22] bugfixes --- examples/html-editor-drawing-hook/index.js | 6 ++-- examples/xml/index.js | 2 +- package.json | 3 +- src/Bindings/DomBinding/DomBinding.js | 4 +-- src/Bindings/DomBinding/domToType.js | 37 +++++++++----------- src/Bindings/DomBinding/filter.js | 2 +- src/Bindings/DomBinding/util.js | 37 +++++++++++++++++--- src/MessageHandler/integrateRemoteStructs.js | 2 +- src/Struct/GC.js | 2 +- src/Struct/Item.js | 2 +- src/Struct/Type.js | 2 ++ src/Types/YArray/YArray.js | 2 +- src/Types/YXml/YXmlFragment.js | 2 ++ src/Types/YXml/YXmlHook.js | 2 +- src/Types/YXml/YXmlText.js | 2 ++ src/Util/relativePosition.js | 6 +++- src/Util/structReferences.js | 2 +- src/Y.dist.js | 6 ++++ test/DeleteStore.tests.js | 1 - tests-lib/helper.js | 37 +------------------- 20 files changed, 82 insertions(+), 77 deletions(-) diff --git a/examples/html-editor-drawing-hook/index.js b/examples/html-editor-drawing-hook/index.js index dca6a6d3..7da324bc 100644 --- a/examples/html-editor-drawing-hook/index.js +++ b/examples/html-editor-drawing-hook/index.js @@ -1,7 +1,7 @@ /* global Y, d3 */ const hooks = { - "magic-drawing": { + 'magic-drawing': { fillType: function (dom, type) { initDrawingBindings(type, dom) }, @@ -19,7 +19,7 @@ window.onload = function () { window.addMagicDrawing = function addMagicDrawing () { let mt = document.createElement('magic-drawing') - mt.dataset.yjsHook = 'magic-drawing' + mt.setAttribute('data-yjs-hook', 'magic-drawing') document.body.append(mt) } @@ -30,7 +30,7 @@ var renderPath = d3.svg.line() function initDrawingBindings (type, dom) { dom.contentEditable = 'false' - dom.dataset.yjsHook = 'magic-drawing' + dom.setAttribute('data-yjs-hook', 'magic-drawing') var drawing = type.get('drawing') if (drawing === undefined) { drawing = type.set('drawing', new Y.Array()) diff --git a/examples/xml/index.js b/examples/xml/index.js index b6c7596a..02b33561 100644 --- a/examples/xml/index.js +++ b/examples/xml/index.js @@ -9,5 +9,5 @@ let y = new Y('xml-example', { window.yXml = y // bind xml type to a dom, and put it in body -window.sharedDom = y.define('xml', Y.XmlElement).getDom() +window.sharedDom = y.define('xml', Y.XmlElement).toDom() document.body.appendChild(window.sharedDom) diff --git a/package.json b/package.json index 9de600a2..309b5929 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "files": [ "y.*", "src/*", - ".esdoc.json" + ".esdoc.json", + "docs/*" ], "standard": { "ignore": [ diff --git a/src/Bindings/DomBinding/DomBinding.js b/src/Bindings/DomBinding/DomBinding.js index ae5a2894..1568f08d 100644 --- a/src/Bindings/DomBinding/DomBinding.js +++ b/src/Bindings/DomBinding/DomBinding.js @@ -51,9 +51,9 @@ export default class DomBinding extends Binding { this.filter = opts.filter || defaultFilter // set initial value target.innerHTML = '' - for (let child of type) { + type.forEach(child => { target.insertBefore(child.toDom(opts.document, opts.hooks, this), null) - } + }) this._typeObserver = typeObserver.bind(this) this._domObserver = (mutations) => { domObserver.call(this, mutations, opts.document) diff --git a/src/Bindings/DomBinding/domToType.js b/src/Bindings/DomBinding/domToType.js index b906b53b..a81583f7 100644 --- a/src/Bindings/DomBinding/domToType.js +++ b/src/Bindings/DomBinding/domToType.js @@ -1,35 +1,37 @@ import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' -import { createAssociation } from './util.js' -import { filterDomAttributes } from './filter.js' +import { createAssociation, domsToTypes } from './util.js' +import { filterDomAttributes, defaultFilter } from './filter.js' /** * Creates a Yjs type (YXml) based on the contents of a DOM Element. * * @param {Element|TextNode} element The DOM Element - * @param {?Document} _document Optional. Provide the global document object. - * @param {?DomBinding} binding This property should only be set if the type - * is going to be bound with the dom-binding. + * @param {?Document} _document Optional. Provide the global document object + * @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks + * @param {Filter} [filter=defaultFilter] Optional. Dom element filter + * @param {?DomBinding} binding Warning: This property is for internal use only! * @return {YXmlElement | YXmlText} */ -export default function domToType (element, _document = document, binding) { +export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) { let type switch (element.nodeType) { case _document.ELEMENT_NODE: - let hookName = element.dataset.yjsHook + let hookName = null let hook // configure `hookName !== undefined` if element is a hook. - if (hookName !== undefined) { - hook = binding.opts.hooks[hookName] + if (element.hasAttribute('data-yjs-hook')) { + hookName = element.getAttribute('data-yjs-hook') + hook = hooks[hookName] if (hook === undefined) { console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`) - delete element.dataset.yjsHook - hookName = undefined + delete element.removeAttribute('data-yjs-hook') + hookName = null } } - if (hookName === undefined) { + if (hookName === null) { // Not a hook - const attrs = filterDomAttributes(element, binding.filter) + const attrs = filterDomAttributes(element, filter) if (attrs === null) { type = false } else { @@ -37,14 +39,7 @@ export default function domToType (element, _document = document, binding) { attrs.forEach((val, key) => { type.setAttribute(key, val) }) - const children = [] - for (let elem of element.childNodes) { - const type = domToType(elem, _document, binding) - if (type !== false) { - children.push(type) - } - } - type.insert(0, children) + type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding)) } } else { // Is a hook diff --git a/src/Bindings/DomBinding/filter.js b/src/Bindings/DomBinding/filter.js index 4fea3f84..08c208e7 100644 --- a/src/Bindings/DomBinding/filter.js +++ b/src/Bindings/DomBinding/filter.js @@ -14,7 +14,7 @@ export function defaultFilter (nodeName, attrs) { } /** - * + * */ export function filterDomAttributes (dom, filter) { const attrs = new Map() diff --git a/src/Bindings/DomBinding/util.js b/src/Bindings/DomBinding/util.js index e3f0ed0e..b4f6148d 100644 --- a/src/Bindings/DomBinding/util.js +++ b/src/Bindings/DomBinding/util.js @@ -17,7 +17,10 @@ export function iterateUntilUndeleted (item) { * Removes an association (the information that a DOM element belongs to a * type). * - * @private + * @param {DomBinding} domBinding The binding object + * @param {Element} dom The dom that is to be associated with type + * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom + * */ export function removeAssociation (domBinding, dom, type) { domBinding.domToType.delete(dom) @@ -28,7 +31,10 @@ export function removeAssociation (domBinding, dom, type) { * Creates an association (the information that a DOM element belongs to a * type). * - * @private + * @param {DomBinding} domBinding The binding object + * @param {Element} dom The dom that is to be associated with type + * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom + * */ export function createAssociation (domBinding, dom, type) { if (domBinding !== undefined) { @@ -37,6 +43,24 @@ export function createAssociation (domBinding, dom, type) { } } +/** + * If oldDom is associated with a type, associate newDom with the type and + * forget about oldDom. If oldDom is not associated with any type, nothing happens. + * + * @param {DomBinding} domBinding The binding object + * @param {Element} oldDom The existing dom + * @param {Element} newDom The new dom object + */ +export function switchAssociation (domBinding, oldDom, newDom) { + if (domBinding !== undefined) { + const type = domBinding.domToType.get(oldDom) + if (type !== undefined) { + removeAssociation(domBinding, oldDom, type) + createAssociation(domBinding, newDom, type) + } + } +} + /** * Insert Dom Elements after one of the children of this YXmlFragment. * The Dom elements will be bound to a new YXmlElement and inserted at the @@ -54,14 +78,19 @@ export function createAssociation (domBinding, dom, type) { * @private */ export function insertDomElementsAfter (type, prev, doms, _document, binding) { + const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding) + return type.insertAfter(prev, types) +} + +export function domsToTypes (doms, _document, hooks, filter, binding) { const types = [] for (let dom of doms) { - const t = domToType(dom, _document, binding) + const t = domToType(dom, _document, hooks, filter, binding) if (t !== false) { types.push(t) } } - return type.insertAfter(prev, types) + return types } /** diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index d2067f3c..390f0924 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -1,7 +1,7 @@ import { getStruct } from '../Util/structReferences.js' import BinaryDecoder from '../Util/Binary/Decoder.js' import { logID } from './messageToString.js' -import GC from '../Struct/GC.js'; +import GC from '../Struct/GC.js' class MissingEntry { constructor (decoder, missing, struct) { diff --git a/src/Struct/GC.js b/src/Struct/GC.js index a23d55b6..6db86017 100644 --- a/src/Struct/GC.js +++ b/src/Struct/GC.js @@ -91,4 +91,4 @@ export default class GC { gc._length = this._length - diff return gc } -} \ No newline at end of file +} diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 1ff928f2..9f17d1e7 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -238,7 +238,7 @@ export default class Item { } _gcChildren (y) {} - + _gc (y) { const gc = new GC() gc._id = this._id diff --git a/src/Struct/Type.js b/src/Struct/Type.js index 44f075ad..6202454b 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -213,6 +213,8 @@ export default class Type extends Item { * @param {Y} y The Yjs instance * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. + * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage + * collect the children of this type. */ _delete (y, createDelete, gcChildren) { if (gcChildren === undefined) { diff --git a/src/Types/YArray/YArray.js b/src/Types/YArray/YArray.js index 27af984c..5914dc7b 100644 --- a/src/Types/YArray/YArray.js +++ b/src/Types/YArray/YArray.js @@ -198,7 +198,7 @@ export default class YArray extends Type { content = this._item._content[this._itemElement++] } return { - value: [this._count, content], + value: content, done: false } }, diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index 827356a0..3c450756 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -123,6 +123,8 @@ export default class YXmlFragment extends YArray { * @param {Y} y The Yjs instance * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. + * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage + * collect the children of this type. * * @private */ diff --git a/src/Types/YXml/YXmlHook.js b/src/Types/YXml/YXmlHook.js index 445f107b..08ad5935 100644 --- a/src/Types/YXml/YXmlHook.js +++ b/src/Types/YXml/YXmlHook.js @@ -52,7 +52,7 @@ export default class YXmlHook extends YMap { } else { dom = document.createElement(this.hookName) } - dom.dataset.yjsHook = this.hookName + dom.setAttribute('data-yjs-hook', this.hookName) createAssociation(binding, dom, this) return dom } diff --git a/src/Types/YXml/YXmlText.js b/src/Types/YXml/YXmlText.js index c0339806..4c34c982 100644 --- a/src/Types/YXml/YXmlText.js +++ b/src/Types/YXml/YXmlText.js @@ -35,6 +35,8 @@ export default class YXmlText extends YText { * @param {Y} y The Yjs instance * @param {boolean} createDelete Whether to propagate a message that this * Type was deleted. + * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage + * collect the children of this type. * * @private */ diff --git a/src/Util/relativePosition.js b/src/Util/relativePosition.js index 05b6b5d9..df85b00c 100644 --- a/src/Util/relativePosition.js +++ b/src/Util/relativePosition.js @@ -1,5 +1,6 @@ import ID from './ID/ID.js' import RootID from './ID/RootID.js' +import GC from '../Struct/GC.js' // TODO: Implement function to describe ranges @@ -76,6 +77,9 @@ export function fromRelativePosition (y, rpos) { id = new RootID(rpos[3], rpos[4]) } const type = y.os.get(id) + if (type === null || type.constructor === GC) { + return null + } return { type, offset: type.length @@ -84,7 +88,7 @@ export function fromRelativePosition (y, rpos) { let offset = 0 let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val const parent = struct._parent - if (parent._deleted) { + if (struct.constructor === GC || parent._deleted) { return null } if (!struct._deleted) { diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index a79d8c71..0c4f7182 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -56,4 +56,4 @@ registerStruct(7, YXmlElement) registerStruct(8, YXmlText) registerStruct(9, YXmlHook) -registerStruct(12, GC) \ No newline at end of file +registerStruct(12, GC) diff --git a/src/Y.dist.js b/src/Y.dist.js index 92df64c7..6074cf0e 100644 --- a/src/Y.dist.js +++ b/src/Y.dist.js @@ -20,6 +20,8 @@ import DomBinding from './Bindings/DomBinding/DomBinding.js' import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' import debug from 'debug' +import domToType from './Bindings/DomBinding/domToType.js' +import { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js' // TODO: The following assignments should be moved to yjs-dist Y.AbstractConnector = Connector @@ -36,6 +38,10 @@ Y.TextareaBinding = TextareaBinding Y.QuillBinding = QuillBinding Y.DomBinding = DomBinding +DomBinding.domToType = domToType +DomBinding.domsToTypes = domsToTypes +DomBinding.switchAssociation = switchAssociation + Y.utils = { BinaryDecoder, UndoManager, diff --git a/test/DeleteStore.tests.js b/test/DeleteStore.tests.js index 52e54382..5f5c3a7a 100644 --- a/test/DeleteStore.tests.js +++ b/test/DeleteStore.tests.js @@ -1,5 +1,4 @@ import { test } from '../node_modules/cutest/cutest.mjs' -import simpleDiff from '../src/Util/simpleDiff.js' import Chance from 'chance' import DeleteStore from '../src/Store/DeleteStore.js' import ID from '../src/Util/ID/ID.js' diff --git a/tests-lib/helper.js b/tests-lib/helper.js index a8425a9d..fe59c55f 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -8,7 +8,7 @@ import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' import Quill from 'quill' -import GC from '../src/Struct/GC.js'; +import GC from '../src/Struct/GC.js' export const Y = _Y @@ -42,41 +42,6 @@ function getDeleteSet (y) { return ds } -// TODO: remove? -export function attrsObject (dom) { - let keys = [] - let yxml = dom._yxml - for (let i = 0; i < dom.attributes.length; i++) { - keys.push(dom.attributes[i].name) - } - keys = yxml._domFilter(dom, keys) - let obj = {} - for (let i = 0; i < keys.length; i++) { - let key = keys[i] - obj[key] = dom.getAttribute(key) - } - return obj -} - -// TODO: remove? -export function domToJson (dom) { - if (dom.nodeType === document.TEXT_NODE) { - return dom.textContent - } else if (dom.nodeType === document.ELEMENT_NODE) { - let attributes = attrsObject(dom) - let children = Array.from(dom.childNodes.values()) - .filter(d => d._yxml !== false) - .map(domToJson) - return { - name: dom.nodeName, - children: children, - attributes: attributes - } - } else { - throw new Error('Unsupported node type') - } -} - /* * 1. reconnect and flush all * 2. user 0 gc