@bayou/doc-common
Advanced tools
Comparing version 1.0.6 to 1.2.4
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -14,2 +14,8 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
/** | ||
* {Logger} Logger for this module. **Note:** Just used for some temporary | ||
* debugging stuff. | ||
*/ | ||
const log = new Logger('body-delta'); | ||
/** | ||
* Always-frozen list of body OT operations. This uses Quill's `Delta` class to | ||
@@ -155,2 +161,7 @@ * implement the "interesting" OT functionality, however this class does _not_ | ||
if (wantDocument && !result.isDocument()) { | ||
// **TODO:** Remove this logging once we track down why we're seeing this | ||
// error. | ||
log.event.badComposeOrig(this, other, result); | ||
log.event.badComposeQuill(quillThis.ops, quillOther.ops, quillResult.ops); | ||
throw Errors.badUse('Inappropriate `other` for composition given `wantDocument === true`.'); | ||
@@ -198,4 +209,5 @@ } | ||
if ((typeof quillDelta === 'object') && (quillDelta.constructor.name === 'Delta')) { | ||
// The version of `Delta` used by Quill is different than the one we | ||
// specified in our `package.json`. Even though it will often happen | ||
// The version of `Delta` used by Quill is different than the one which | ||
// is specified in the configuration hook | ||
// {@link @bayou/config-common/Text}. Even though it will often happen | ||
// to work if we just let it slide (e.g. by snarfing `ops` out of the | ||
@@ -207,20 +219,21 @@ // object and running with it), we don't want to end up shipping two | ||
// yourself looking at this error, the likely right thing to do is | ||
// look at `package.json` in the version of Quill you are using, and | ||
// update what is returned from | ||
// {@link @bayou/config-common/Text#Delta} to match what Quill has as | ||
// its `quill-delta` dependency. | ||
// look at the lines logged just before the error was thrown, and change | ||
// either how `Text` is configured to match the incoming `Delta` version | ||
// _or_ change where the incoming `Delta` version is used to match what | ||
// got configured for `Text`. Of particular note, if you are using a | ||
// version of `Quill` in distribution form, it bundles within itself a | ||
// version of `Delta`, and you will want to make sure that `Text` uses | ||
// `Quill.import('delta')` to get it hooked up. | ||
// **TODO:** Because reasons, this complaint has been demoted to | ||
// from "actual error" to "stern warning," for the time being. The | ||
// `throw` should be restored at the earliest opportunity, along with | ||
// removing the surrounding bits that let this case fall through. | ||
//throw Errors.badUse('Divergent versions of `quill-delta` package.'); | ||
if (!BodyDelta._divergentComplaintMade) { | ||
// NB: This is the only code in this module that uses `see-all`. | ||
BodyDelta._divergentComplaintMade = true; | ||
new Logger('doc-common').warn('Divergent versions of `quill-delta` package!'); | ||
} | ||
} else { | ||
throw e; | ||
// This is an attempt to get a stack trace each from both "our" `Delta` | ||
// and the incoming `Delta`, as a possible breadcrumb for folks running | ||
// into this problem. | ||
log.error('Divergent versions of `quill-delta` package.'); | ||
BodyDelta._logWhichDelta('ours', Text.Delta); | ||
BodyDelta._logWhichDelta('incoming', quillDelta.constructor); | ||
throw Errors.badUse('Divergent versions of `quill-delta` package.'); | ||
} | ||
throw e; | ||
} | ||
@@ -230,2 +243,30 @@ | ||
} | ||
/** | ||
* Helper used when making the "divergent" complaint, which aims to log an | ||
* error pointing at the source of one of the versions of `Delta`. | ||
* | ||
* @param {string} label Short label indicating which version this is. | ||
* @param {class} Delta A version of the `Delta` class. | ||
*/ | ||
static _logWhichDelta(label, Delta) { | ||
const delta = new Delta([{ insert: 'x' }]); | ||
let trace; | ||
try { | ||
delta.forEach(() => { throw new Error('x'); }); | ||
} catch (e) { | ||
trace = e.stack; | ||
} | ||
for (const line of trace.split('\n')) { | ||
const match = line.match(/at Delta[^(]+\(([^:)]+)[:)]/); | ||
if (match) { | ||
log.info(`${label}: ${match[1]}`); | ||
return; | ||
} | ||
} | ||
log.error(`Could not determine \`Delta\` version "${label}" from stack trace!`, trace); | ||
} | ||
} |
@@ -1,5 +0,7 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
// Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import GraphemeSplitter from 'grapheme-splitter'; | ||
import { BaseOp } from '@bayou/ot-common'; | ||
@@ -9,2 +11,7 @@ import { TInt, TObject, TString } from '@bayou/typecheck'; | ||
// Instance of the grapheme splitter library | ||
// used to properly count the length of | ||
// special characters, such as emojis. | ||
const splitter = new GraphemeSplitter(); | ||
/** | ||
@@ -198,2 +205,36 @@ * Operation on a text document body. | ||
/** | ||
* Returns the length of an Op. | ||
* @param {boolean} [textOnly = false] An optional switch to determine whether | ||
* to only take text into account when determing the length of an Op. | ||
* @returns {integer} The length of the Op. Embeds count as 1. | ||
*/ | ||
getLength(textOnly = false) { | ||
const opProps = this.props; | ||
switch (opProps.opName) { | ||
case BodyOp.CODE_embed: { | ||
if (textOnly) { | ||
return 0; | ||
} | ||
return 1; | ||
} | ||
case BodyOp.CODE_text: { | ||
// In order to properly determine the length of | ||
// a given piece of text, a special library was | ||
// needed. This is because special characters, | ||
// such as emojis, often have a "length" of more | ||
// than 1. We are more concerned with "spaces | ||
// in the document". | ||
return splitter.countGraphemes(opProps.text); | ||
} | ||
default: { | ||
return 0; | ||
} | ||
} | ||
} | ||
/** | ||
* Indicates whether or not this instance is an "insert" operation of some | ||
@@ -279,2 +320,13 @@ * sort. | ||
} | ||
/** | ||
* Subclass-specific implementation of {@link #isValidPayload}. | ||
* | ||
* @param {Functor} payload_unused The would-be payload for an instance. | ||
* @returns {boolean} `true` if `payload` is valid, or `false` if not. | ||
*/ | ||
static _impl_isValidPayload(payload_unused) { | ||
// **TODO:** Fill this in! | ||
return true; | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -6,2 +6,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import { BaseSnapshot } from '@bayou/ot-common'; | ||
import { Errors } from '@bayou/util-common'; | ||
@@ -30,2 +31,15 @@ import BodyChange from './BodyChange'; | ||
/** | ||
* {int} The length of this document, where the count is made | ||
* up of text and embeds. An embed has a length of 1. | ||
*/ | ||
get length() { | ||
const bodyContentOps = this.contents.ops; | ||
// Assume all contents are `text` or `embed` | ||
const length = bodyContentOps.reduce((sum, op) => sum + op.getLength(), 0); | ||
return length; | ||
} | ||
/** | ||
* Main implementation of {@link #diff}, which produces a delta (not a | ||
@@ -47,2 +61,48 @@ * change). | ||
/** | ||
* Implementation of {@link validateChange}, which | ||
* will perform semantic validation checks on a body change | ||
* in the context of a snapshot. | ||
* | ||
* @param {BodyChange} change The change being validated. | ||
* @throws {Error} A validation error if semantic validation | ||
* performed on a given change fails. | ||
*/ | ||
_impl_validateChange(change) { | ||
const ops = change.delta.ops; | ||
for (const op of ops) { | ||
const opProps = op.props; | ||
const { opName } = opProps; | ||
// TODO: validate `delete` in a similar way | ||
switch (opName) { | ||
case 'retain': { | ||
this._validateRetainOp(op, this.length); | ||
break; | ||
} | ||
default: { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Performs semantic validation on a retain OP. | ||
* Checks to make sure that the given retain OP's count | ||
* does not exceed the document body length | ||
* @param {BodyOp} retainOp A retain Op. | ||
* @param {int} bodyLength The maximum length that can be retained. | ||
* @throws {Error} A validation error if any semantic validation fails | ||
* on given `retain` op. | ||
*/ | ||
_validateRetainOp(retainOp, bodyLength) { | ||
const { count } = retainOp.props; | ||
if (count > bodyLength) { | ||
throw Errors.badData(`Attempting to retain ${count} when document length is ${bodyLength}`); | ||
} | ||
} | ||
/** | ||
* {class} Class (constructor function) of change objects to be used with | ||
@@ -49,0 +109,0 @@ * instances of this class. |
127
Caret.js
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -10,2 +10,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import CaretDelta from './CaretDelta'; | ||
import CaretId from './CaretId'; | ||
import CaretOp from './CaretOp'; | ||
@@ -17,3 +18,3 @@ | ||
* | ||
* **Note:** `sessionId` is not included, because that's separate from the | ||
* **Note:** The caret's ID is not included, because that's separate from the | ||
* caret's "fields" per se. | ||
@@ -26,10 +27,17 @@ * | ||
const CARET_FIELDS = new Map([ | ||
['authorId', TString.check.bind(TString)], | ||
['color', ColorUtil.checkCss], | ||
['index', TInt.nonNegative], | ||
['lastActive', Timestamp.check.bind(Timestamp)], | ||
['revNum', RevisionNumber.check], | ||
['index', TInt.nonNegative], | ||
['length', TInt.nonNegative], | ||
['color', ColorUtil.checkCss] | ||
['revNum', RevisionNumber.check] | ||
]); | ||
/** | ||
* {string} Special value for the ID which is only allowed for the default | ||
* caret. | ||
*/ | ||
const DEFAULT_ID = '<no_id>'; | ||
/** | ||
* {Caret|null} An instance with all default values. Initialized in the static | ||
@@ -46,4 +54,4 @@ * method of the same name. | ||
* names, and the like, is meant to be a synecdochal metaphor for all | ||
* information about a session, including the human driving it. The caret per | ||
* se is merely the most blatant aspect of it. | ||
* information about a session from the user's perspective, including the human | ||
* driving it. The caret per se is merely the most blatant aspect of it. | ||
*/ | ||
@@ -54,3 +62,5 @@ export default class Caret extends CommonBase { | ||
if (DEFAULT === null) { | ||
DEFAULT = new Caret('no_session', | ||
// **Note:** There is no default for `authorId`, which is what makes it | ||
// end up getting required when constructing a new instance from scratch. | ||
DEFAULT = new Caret(DEFAULT_ID, | ||
{ | ||
@@ -96,24 +106,30 @@ lastActive: Timestamp.now(), | ||
/** | ||
* Constructs an instance. Only the first argument (`sessionIdOrBase`) is | ||
* required, and it is not necessary to specify all the fields in `fields`. | ||
* Fields not listed are derived from the base caret (first argument) if | ||
* specified as such, or from the default value `Caret.DEFAULT` if the first | ||
* argument is a session ID. | ||
* Constructs an instance. Only the first argument (`idOrBase`) is required, | ||
* and it is not necessary to specify all the fields in `fields`. Fields not | ||
* listed are derived from the base caret (first argument) if specified as | ||
* such, or from the default value {@link #DEFAULT} if the first argument is | ||
* an ID. | ||
* | ||
* @param {string|Caret} sessionIdOrBase Session ID that identifies the caret, | ||
* or a base caret instance which provides the session and default values | ||
* for fields. | ||
* **Note:** {@link #DEFAULT} does not bind an `authorId`, which means that | ||
* that field must be specified when creating an instance "from scratch." | ||
* | ||
* @param {string|Caret} idOrBase Caret ID, or a base caret instance which | ||
* provides the ID and default values for fields. | ||
* @param {object} [fields = {}] Fields of the caret, as plain object mapping | ||
* field names to values. | ||
*/ | ||
constructor(sessionIdOrBase, fields = {}) { | ||
let sessionId; | ||
constructor(idOrBase, fields = {}) { | ||
let id; | ||
let newFields; | ||
if (sessionIdOrBase instanceof Caret) { | ||
newFields = new Map(sessionIdOrBase._fields); | ||
sessionId = sessionIdOrBase.sessionId; | ||
if (idOrBase instanceof Caret) { | ||
newFields = new Map(idOrBase._fields); | ||
id = idOrBase.id; | ||
} else if (DEFAULT !== null) { | ||
newFields = new Map(DEFAULT._fields); | ||
id = CaretId.check(idOrBase); | ||
} else { | ||
newFields = DEFAULT ? new Map(DEFAULT._fields) : new Map(); | ||
sessionId = TString.nonEmpty(sessionIdOrBase); | ||
// If we're here, it means that `DEFAULT` is currently being initialized. | ||
newFields = new Map(); | ||
id = TString.check(idOrBase); | ||
} | ||
@@ -125,4 +141,4 @@ | ||
/** {string} The session ID. */ | ||
this._sessionId = sessionId; | ||
/** {string} The caret ID. */ | ||
this._id = id; | ||
@@ -136,3 +152,3 @@ /** {Map<string,*>} Map of all of the caret fields, from name to value. */ | ||
if (DEFAULT && (newFields.size !== DEFAULT._fields.size)) { | ||
if (DEFAULT && (newFields.size !== CARET_FIELDS.size)) { | ||
throw Errors.badUse(`Missing field.`); | ||
@@ -145,2 +161,9 @@ } | ||
/** | ||
* {string} ID of the author responsible for this caret. | ||
*/ | ||
get authorId() { | ||
return this._fields.get('authorId'); | ||
} | ||
/** | ||
* {string} The color to be used when annotating this selection. It is in CSS | ||
@@ -154,2 +177,10 @@ * three-byte hex format (e.g. `'#ffeab9'`). | ||
/** | ||
* {string} ID of the caret. This uniquely identifies this caret within the | ||
* context of a specific document. | ||
*/ | ||
get id() { | ||
return this._id; | ||
} | ||
/** | ||
* {Int} The zero-based leading position of this caret / selection. | ||
@@ -162,3 +193,3 @@ */ | ||
/** | ||
* {Timestamp} The moment in time when this session was last active. | ||
* {Timestamp} The moment in time when this caret was last active. | ||
*/ | ||
@@ -186,13 +217,5 @@ get lastActive() { | ||
/** | ||
* {string} Opaque reference to be used with other APIs to get information | ||
* about the author whose caret this is. | ||
*/ | ||
get sessionId() { | ||
return this._sessionId; | ||
} | ||
/** | ||
* Composes the given `delta` on top of this instance, producing a new | ||
* instance. The operations in `delta` must all be `setField` ops for the same | ||
* `sessionId` as this instance. | ||
* `id` as this instance. | ||
* | ||
@@ -212,4 +235,4 @@ * @param {CaretDelta} delta Delta to apply. | ||
throw Errors.badUse(`Invalid operation name: ${props.opName}`); | ||
} else if (props.sessionId !== this.sessionId) { | ||
throw Errors.badUse('Mismatched session ID.'); | ||
} else if (props.caretId !== this.id) { | ||
throw Errors.badUse('Mismatched ID.'); | ||
} | ||
@@ -235,3 +258,3 @@ | ||
return [this._sessionId, fields]; | ||
return [this._id, fields]; | ||
} | ||
@@ -251,3 +274,3 @@ | ||
* @param {Caret} newerCaret Caret to take the difference from. It must have | ||
* the same `sessionId` as this instance. | ||
* the same `id` as this instance. | ||
* @returns {CaretDelta} Delta which represents the difference between | ||
@@ -259,24 +282,24 @@ * `newerCaret` and this instance. | ||
const sessionId = this.sessionId; | ||
const id = this.id; | ||
if (sessionId !== newerCaret.sessionId) { | ||
throw Errors.badUse('Cannot `diff` carets with mismatched `sessionId`.'); | ||
if (id !== newerCaret.id) { | ||
throw Errors.badUse('Cannot `diff` carets with mismatched `id`.'); | ||
} | ||
return this.diffFields(newerCaret, sessionId); | ||
return this.diffFields(newerCaret, id); | ||
} | ||
/** | ||
* Like `diff()`, except does _not_ check to see if the two instances' | ||
* `sessionId`s match. That is, it only looks at the fields. | ||
* Like `diff()`, except does _not_ check to see if the two instances' `id`s | ||
* match. That is, it only looks at the fields. | ||
* | ||
* @param {Caret} newerCaret Caret to take the difference from. | ||
* @param {string} sessionId Session ID to use for the ops in the result. | ||
* @param {string} id ID to use for the ops in the result. | ||
* @returns {CaretDelta} Delta which represents the difference between | ||
* `newerCaret` and this instance, _not_ including any difference in | ||
* `sessionId`, if any. | ||
* `id`, if any. | ||
*/ | ||
diffFields(newerCaret, sessionId) { | ||
diffFields(newerCaret, id) { | ||
Caret.check(newerCaret); | ||
TString.nonEmpty(sessionId); | ||
CaretId.check(id); | ||
@@ -288,3 +311,3 @@ const fields = this._fields; | ||
if (!Caret._equalFields(v, fields.get(k))) { | ||
ops.push(CaretOp.op_setField(sessionId, k, v)); | ||
ops.push(CaretOp.op_setField(id, k, v)); | ||
} | ||
@@ -310,3 +333,3 @@ } | ||
if (this._sessionId !== other._sessionId) { | ||
if (this._id !== other._id) { | ||
return false; | ||
@@ -313,0 +336,0 @@ } |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -16,4 +16,4 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
* **Note:** To be valid as a document delta, the set of operations must (a) | ||
* only consist of `beginSession` ops, and (b) not mention any given session ID | ||
* more than once. | ||
* only consist of `add` ops, and (b) not mention any given caret ID more than | ||
* once. | ||
* | ||
@@ -32,7 +32,7 @@ * Instances of this class are immutable. | ||
_impl_compose(other, wantDocument) { | ||
// Map from each session to an array of ops which apply to it. | ||
const sessions = new Map(); | ||
// Map from each caret to an array of ops which apply to it. | ||
const carets = new Map(); | ||
// Add / replace the ops, first from `this` and then from `other`, as a | ||
// mapping from the session ID. | ||
// mapping from the caret ID. | ||
for (const op of [...this.ops, ...other.ops]) { | ||
@@ -42,18 +42,18 @@ const opProps = op.props; | ||
switch (opProps.opName) { | ||
case CaretOp.CODE_beginSession: { | ||
// Clear out the session except for this op, because no earlier op | ||
// could possibly affect the result. | ||
sessions.set(opProps.caret.sessionId, [op]); | ||
case CaretOp.CODE_add: { | ||
// Clear out the caret except for this op, because no earlier op could | ||
// possibly affect the result. | ||
carets.set(opProps.caret.id, [op]); | ||
break; | ||
} | ||
case CaretOp.CODE_endSession: { | ||
case CaretOp.CODE_delete: { | ||
if (wantDocument) { | ||
// Document deltas don't remember session deletions. | ||
sessions.delete(opProps.sessionId); | ||
// Document deltas don't remember caret deletions. | ||
carets.delete(opProps.caretId); | ||
} else { | ||
// Clear out the session; same reason as `BEGIN_SESSION` above. We | ||
// _do_ keep the op, because the fact of a deletion needs to be part | ||
// of the final composed result. | ||
sessions.set(opProps.sessionId, [op]); | ||
// Clear out the caret; same reason as `add` above. We _do_ keep the | ||
// op, because the fact of a deletion needs to be part of the final | ||
// composed result. | ||
carets.set(opProps.caretId, [op]); | ||
} | ||
@@ -64,22 +64,21 @@ break; | ||
case CaretOp.CODE_setField: { | ||
const sessionId = opProps.sessionId; | ||
const ops = sessions.get(sessionId); | ||
let handled = false; | ||
const caretId = opProps.caretId; | ||
const ops = carets.get(caretId); | ||
let handled = false; | ||
if (!ops) { | ||
// This is a "naked" set (no corresponding `BEGIN_SESSION` in the | ||
// result. Just start off an array with it. | ||
sessions.set(sessionId, [op]); | ||
// This is a "naked" set (no corresponding `add` in the result). | ||
// Just start off an array with it. | ||
carets.set(caretId, [op]); | ||
handled = true; | ||
} else if (ops.length === 1) { | ||
// We have a single-element array this session. It might be a | ||
// `BEGIN_SESSION` or `END_SESSION`, in which case we can do | ||
// something special. | ||
// We have a single-element array for this caret. It might be a | ||
// `add` or an `delete`, in which case we can do something special. | ||
const op0Props = ops[0].props; | ||
if (op0Props.opName === CaretOp.CODE_beginSession) { | ||
if (op0Props.opName === CaretOp.CODE_add) { | ||
// Integrate the new value into the caret. | ||
const caret = op0Props.caret.compose(new CaretDelta([op])); | ||
ops[0] = CaretOp.op_beginSession(caret); | ||
ops[0] = CaretOp.op_add(caret); | ||
handled = true; | ||
} else if (op0Props.opName === CaretOp.CODE_endSession) { | ||
} else if (op0Props.opName === CaretOp.CODE_delete) { | ||
// We ignore set-after-end. A bit philosophical, but what does | ||
@@ -114,3 +113,3 @@ // it even mean to set a value on a nonexistent thing? | ||
const allOps = [].concat(...sessions.values()); | ||
const allOps = [].concat(...carets.values()); | ||
return new CaretDelta(allOps); | ||
@@ -132,10 +131,10 @@ } | ||
switch (opProps.opName) { | ||
case CaretOp.CODE_beginSession: { | ||
const sessionId = opProps.caret.sessionId; | ||
case CaretOp.CODE_add: { | ||
const caretId = opProps.caret.id; | ||
if (ids.has(sessionId)) { | ||
if (ids.has(caretId)) { | ||
return false; | ||
} | ||
ids.add(sessionId); | ||
ids.add(caretId); | ||
break; | ||
@@ -142,0 +141,0 @@ } |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -6,6 +6,6 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import { BaseOp } from '@bayou/ot-common'; | ||
import { TString } from '@bayou/typecheck'; | ||
import { Errors } from '@bayou/util-common'; | ||
import Caret from './Caret'; | ||
import CaretId from './CaretId'; | ||
@@ -16,10 +16,10 @@ /** | ||
export default class CaretOp extends BaseOp { | ||
/** {string} Opcode constant for "begin session" operations. */ | ||
static get CODE_beginSession() { | ||
return 'beginSession'; | ||
/** {string} Opcode constant for "add" operations (add a new caret). */ | ||
static get CODE_add() { | ||
return 'add'; | ||
} | ||
/** {string} Opcode constant for "end session" operations. */ | ||
static get CODE_endSession() { | ||
return 'endSession'; | ||
/** {string} Opcode constant for "delete" operations (delete a caret). */ | ||
static get CODE_delete() { | ||
return 'delete'; | ||
} | ||
@@ -33,24 +33,23 @@ | ||
/** | ||
* Constructs a new "begin session" operation. | ||
* Constructs a new "add" operation. | ||
* | ||
* @param {Caret} caret The initial caret for the new session (which includes | ||
* a session ID). | ||
* @param {Caret} caret The caret to add. | ||
* @returns {CaretOp} The corresponding operation. | ||
*/ | ||
static op_beginSession(caret) { | ||
static op_add(caret) { | ||
Caret.check(caret); | ||
return new CaretOp(CaretOp.CODE_beginSession, caret); | ||
return new CaretOp(CaretOp.CODE_add, caret); | ||
} | ||
/** | ||
* Constructs a new "end session" operation. | ||
* Constructs a new "delete" operation. | ||
* | ||
* @param {string} sessionId ID of the session. | ||
* @param {string} caretId ID of the caret which is to be removed. | ||
* @returns {CaretOp} The corresponding operation. | ||
*/ | ||
static op_endSession(sessionId) { | ||
TString.nonEmpty(sessionId); | ||
static op_delete(caretId) { | ||
CaretId.check(caretId); | ||
return new CaretOp(CaretOp.CODE_endSession, sessionId); | ||
return new CaretOp(CaretOp.CODE_delete, caretId); | ||
} | ||
@@ -61,3 +60,3 @@ | ||
* | ||
* @param {string} sessionId Session for the caret to update. | ||
* @param {string} caretId ID of the caret to update. | ||
* @param {string} key Name of the field to update. | ||
@@ -68,7 +67,7 @@ * @param {*} value New value for the so-named field. Type restriction on this | ||
*/ | ||
static op_setField(sessionId, key, value) { | ||
TString.nonEmpty(sessionId); | ||
static op_setField(caretId, key, value) { | ||
CaretId.check(caretId); | ||
Caret.checkField(key, value); | ||
return new CaretOp(CaretOp.CODE_setField, sessionId, key, value); | ||
return new CaretOp(CaretOp.CODE_setField, caretId, key, value); | ||
} | ||
@@ -87,3 +86,3 @@ | ||
switch (opName) { | ||
case CaretOp.CODE_beginSession: { | ||
case CaretOp.CODE_add: { | ||
const [caret] = payload.args; | ||
@@ -93,10 +92,10 @@ return Object.freeze({ opName, caret }); | ||
case CaretOp.CODE_endSession: { | ||
const [sessionId] = payload.args; | ||
return Object.freeze({ opName, sessionId }); | ||
case CaretOp.CODE_delete: { | ||
const [caretId] = payload.args; | ||
return Object.freeze({ opName, caretId }); | ||
} | ||
case CaretOp.CODE_setField: { | ||
const [sessionId, key, value] = payload.args; | ||
return Object.freeze({ opName, sessionId, key, value }); | ||
const [caretId, key, value] = payload.args; | ||
return Object.freeze({ opName, caretId, key, value }); | ||
} | ||
@@ -109,2 +108,13 @@ | ||
} | ||
/** | ||
* Subclass-specific implementation of {@link #isValidPayload}. | ||
* | ||
* @param {Functor} payload_unused The would-be payload for an instance. | ||
* @returns {boolean} `true` if `payload` is valid, or `false` if not. | ||
*/ | ||
static _impl_isValidPayload(payload_unused) { | ||
// **TODO:** Fill this in! | ||
return true; | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -6,3 +6,2 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import { BaseSnapshot } from '@bayou/ot-common'; | ||
import { TString } from '@bayou/typecheck'; | ||
import { Errors } from '@bayou/util-common'; | ||
@@ -13,2 +12,3 @@ | ||
import CaretDelta from './CaretDelta'; | ||
import CaretId from './CaretId'; | ||
import CaretOp from './CaretOp'; | ||
@@ -18,7 +18,7 @@ | ||
/** | ||
* Snapshot of information about all active sessions on a particular document. | ||
* Snapshot of information about all active carets on a particular document. | ||
* Instances of this class are always frozen (immutable). | ||
* | ||
* When thought of in terms of a map, instances of this class can be taken to | ||
* be maps from session ID strings to `Caret` values. | ||
* be maps from caret ID strings to `Caret` values. | ||
*/ | ||
@@ -38,4 +38,4 @@ export default class CaretSnapshot extends BaseSnapshot { | ||
/** | ||
* {Map<string, CaretOp>} Map of session ID to an `op_beginSession` which | ||
* defines the caret for that session. | ||
* {Map<string, CaretOp>} Map of caret ID to an `op_add` which contains a | ||
* caret with that ID. | ||
*/ | ||
@@ -49,4 +49,4 @@ this._carets = new Map(); | ||
switch (opProps.opName) { | ||
case CaretOp.CODE_beginSession: { | ||
this._carets.set(opProps.caret.sessionId, op); | ||
case CaretOp.CODE_add: { | ||
this._carets.set(opProps.caret.id, op); | ||
break; | ||
@@ -77,3 +77,3 @@ } | ||
/** | ||
* Gets an iterator over the `[sessionId, caret]` entries that make up the | ||
* Gets an iterator over the `[caretId, caret]` entries that make up the | ||
* snapshot. | ||
@@ -84,3 +84,3 @@ * | ||
* | ||
* @yields {[string, Caret]} Snapshot entries. The keys are the session IDs, | ||
* @yields {[string, Caret]} Snapshot entries. The keys are the caret IDs, | ||
* and the values are the corresponding caret values. | ||
@@ -91,3 +91,3 @@ */ | ||
const caret = op.props.caret; | ||
yield [caret.sessionId, caret]; | ||
yield [caret.id, caret]; | ||
} | ||
@@ -97,4 +97,4 @@ } | ||
/** | ||
* Gets the caret info for the given session. It is an error if this instance | ||
* has no caret for the indicated session. | ||
* Gets the {@link Caret} with the given ID. It is an error if this instance | ||
* has no caret with that ID. | ||
* | ||
@@ -104,7 +104,7 @@ * **Note:** This differs from the semantics of the `Map` method of the same | ||
* | ||
* @param {string} sessionId Session in question. | ||
* @param {string} caretId ID of the caret in question. | ||
* @returns {Caret} Corresponding caret. | ||
*/ | ||
get(sessionId) { | ||
const found = this.getOrNull(sessionId); | ||
get(caretId) { | ||
const found = this.getOrNull(caretId); | ||
@@ -115,15 +115,16 @@ if (found) { | ||
throw Errors.badUse(`No such session: ${sessionId}`); | ||
throw Errors.badUse(`No such caret: ${caretId}`); | ||
} | ||
/** | ||
* Gets the caret info for the given session, if any. | ||
* Gets the {@link Caret} with the given ID, if this instance in fact stores | ||
* such a caret. | ||
* | ||
* @param {string} sessionId Session in question. | ||
* @param {string} caretId ID of the caret in question. | ||
* @returns {Caret|null} Corresponding caret, or `null` if there is none. | ||
*/ | ||
getOrNull(sessionId) { | ||
TString.nonEmpty(sessionId); | ||
getOrNull(caretId) { | ||
CaretId.check(caretId); | ||
const found = this._carets.get(sessionId); | ||
const found = this._carets.get(caretId); | ||
@@ -155,4 +156,4 @@ return found ? found.props.caret : null; | ||
for (const [sessionId, thisCaret] of thisCarets) { | ||
if (!thisCaret.equals(otherCarets.get(sessionId))) { | ||
for (const [caretId, thisCaret] of thisCarets) { | ||
if (!thisCaret.equals(otherCarets.get(caretId))) { | ||
return false; | ||
@@ -166,3 +167,3 @@ } | ||
/** | ||
* Gets whether or not this instance has a caret for the given session. | ||
* Gets whether or not this instance has a caret with the given ID. | ||
* | ||
@@ -172,11 +173,27 @@ * **Note:** This has identical semantics to the `Map` method of the same | ||
* | ||
* @param {string} sessionId Session in question. | ||
* @returns {boolean} `true` if this instance has a caret for the indicated | ||
* session, or `false` if not. | ||
* @param {string} caretId ID of the caret in question. | ||
* @returns {boolean} `true` if this instance has a caret with the indicated | ||
* ID, or `false` if not. | ||
*/ | ||
has(sessionId) { | ||
return this.getOrNull(sessionId) !== null; | ||
has(caretId) { | ||
return this.getOrNull(caretId) !== null; | ||
} | ||
/** | ||
* Returns a randomly-generated ID which is guaranteed not to be used by any | ||
* caret in this instance. | ||
* | ||
* @returns {string} Available caret ID. | ||
*/ | ||
randomUnusedId() { | ||
// Loop in case we get _very_ unlucky. | ||
for (;;) { | ||
const result = CaretId.randomInstance(); | ||
if (!this.has(result)) { | ||
return result; | ||
} | ||
} | ||
} | ||
/** | ||
* Constructs an instance just like this one, except with an additional or | ||
@@ -193,6 +210,5 @@ * updated reference to the indicated caret. If the given caret (including all | ||
const sessionId = caret.sessionId; | ||
const op = CaretOp.op_beginSession(caret); | ||
const op = CaretOp.op_add(caret); | ||
return op.equals(this._carets.get(sessionId)) | ||
return op.equals(this._carets.get(caret.id)) | ||
? this | ||
@@ -204,32 +220,22 @@ : this.compose(new CaretChange(this.revNum, [op])); | ||
* Constructs an instance just like this one, except without any reference to | ||
* the session indicated by the given caret. If there is no session for the | ||
* given caret, this method returns `this`. | ||
* a caret with the indicated ID. If there is no such caret, this method | ||
* returns `this`. | ||
* | ||
* @param {Caret} caret The caret whose session should not be represented in | ||
* the result. Only the `sessionId` of the caret is consulted; it doesn't | ||
* matter if other caret fields match. | ||
* @param {string|Caret} idOrCaret The ID of the caret which should not be | ||
* represented in the result, or a {@link Caret} whose ID is used for the | ||
* check. (That is, if given a {@link Caret}, only the `id` is consulted; it | ||
* doesn't matter if other fields match. | ||
* @returns {CaretSnapshot} An appropriately-constructed instance. | ||
*/ | ||
withoutCaret(caret) { | ||
Caret.check(caret); | ||
return this.withoutSession(caret.sessionId); | ||
} | ||
withoutCaret(idOrCaret) { | ||
const caretId = (typeof idOrCaret === 'string') | ||
? CaretId.check(idOrCaret) | ||
: Caret.check(idOrCaret).id; | ||
/** | ||
* Constructs an instance just like this one, except without any reference to | ||
* the indicated session. If the session is not represented in this instance, | ||
* this method returns `this`. | ||
* | ||
* @param {string} sessionId ID of the session which should not be represented | ||
* in the result. | ||
* @returns {CaretSnapshot} An appropriately-constructed instance. | ||
*/ | ||
withoutSession(sessionId) { | ||
// This type checks `sessionId`, which is why it's not just run when we need | ||
// to call `compose()`. | ||
const op = CaretOp.op_endSession(sessionId); | ||
return this._carets.has(sessionId) | ||
? this.compose(new CaretChange(this.revNum, [op])) | ||
: this; | ||
if (this._carets.has(caretId)) { | ||
const op = CaretOp.op_delete(caretId); | ||
return this.compose(new CaretChange(this.revNum, [op])); | ||
} else { | ||
return this; | ||
} | ||
} | ||
@@ -253,6 +259,6 @@ | ||
for (const [sessionId, caretOp] of newerCarets) { | ||
const already = this._carets.get(sessionId); | ||
for (const [caretId, caretOp] of newerCarets) { | ||
const already = this._carets.get(caretId); | ||
if (already) { | ||
// The `sessionId` matches the older snapshot. Indicate an update if the | ||
// The `caretId` matches the older snapshot. Indicate an update if the | ||
// values are different. | ||
@@ -266,3 +272,3 @@ if (!already.equals(caretOp)) { | ||
} else { | ||
// The `sessionId` isn't in the older snapshot, so this is an addition. | ||
// The `caretId` isn't in the older snapshot, so this is an addition. | ||
resultOps.push(caretOp); | ||
@@ -274,5 +280,5 @@ } | ||
for (const sessionId of this._carets.keys()) { | ||
if (!newerCarets.get(sessionId)) { | ||
resultOps.push(CaretOp.op_endSession(sessionId)); | ||
for (const caretId of this._carets.keys()) { | ||
if (!newerCarets.get(caretId)) { | ||
resultOps.push(CaretOp.op_delete(caretId)); | ||
} | ||
@@ -285,2 +291,7 @@ } | ||
// TODO: implement caret snapshot specific validation | ||
_impl_validateChange() { | ||
return true; | ||
} | ||
/** | ||
@@ -287,0 +298,0 @@ * {class} Class (constructor function) of change objects to be used with |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -13,5 +13,5 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import CaretDelta from './CaretDelta'; | ||
import CaretId from './CaretId'; | ||
import CaretOp from './CaretOp'; | ||
import CaretSnapshot from './CaretSnapshot'; | ||
import DocumentId from './DocumentId'; | ||
import Property from './Property'; | ||
@@ -22,2 +22,3 @@ import PropertyChange from './PropertyChange'; | ||
import PropertySnapshot from './PropertySnapshot'; | ||
import SessionInfo from './SessionInfo'; | ||
import Timeouts from './Timeouts'; | ||
@@ -34,5 +35,5 @@ | ||
CaretDelta, | ||
CaretId, | ||
CaretOp, | ||
CaretSnapshot, | ||
DocumentId, | ||
Property, | ||
@@ -43,3 +44,4 @@ PropertyChange, | ||
PropertySnapshot, | ||
SessionInfo, | ||
Timeouts | ||
}; |
@@ -7,11 +7,13 @@ { | ||
"dependencies": { | ||
"@bayou/codec": "1.0.6", | ||
"@bayou/config-common": "1.0.6", | ||
"@bayou/ot-common": "1.0.6", | ||
"@bayou/see-all": "1.0.6", | ||
"@bayou/typecheck": "1.0.6", | ||
"@bayou/util-common": "1.0.6" | ||
"@bayou/api-common": "1.2.4", | ||
"@bayou/codec": "1.2.4", | ||
"@bayou/config-common": "1.2.4", | ||
"@bayou/ot-common": "1.2.4", | ||
"@bayou/see-all": "1.2.4", | ||
"@bayou/typecheck": "1.2.4", | ||
"@bayou/util-common": "1.2.4", | ||
"grapheme-splitter": "^1.0.2" | ||
}, | ||
"name": "@bayou/doc-common", | ||
"version": "1.0.6" | ||
"version": "1.2.4" | ||
} |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -76,2 +76,13 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
} | ||
/** | ||
* Subclass-specific implementation of {@link #isValidPayload}. | ||
* | ||
* @param {Functor} payload_unused The would-be payload for an instance. | ||
* @returns {boolean} `true` if `payload` is valid, or `false` if not. | ||
*/ | ||
static _impl_isValidPayload(payload_unused) { | ||
// **TODO:** Fill this in! | ||
return true; | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -14,3 +14,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
/** | ||
* Snapshot of information about all active sessions on a particular document. | ||
* Snapshot of information about all the properties of a particular document. | ||
* Instances of this class are always frozen (immutable). | ||
@@ -237,2 +237,7 @@ * | ||
// TODO: implement property snapshot specific validation | ||
_impl_validateChange() { | ||
return true; | ||
} | ||
/** | ||
@@ -239,0 +244,0 @@ * {class} Class (constructor function) of change objects to be used with |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -65,2 +65,25 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
}); | ||
it('should reject arguments that are instances of the wrong `Delta` class', () => { | ||
// This test confirms that the code properly detects when there's more | ||
// than one class named `Delta` in the system and the one _not_ used by | ||
// this module is passed in here. Historically speaking, the check has | ||
// caught actual build problems. | ||
// The check doesn't really know that it got a bona fide implementation of | ||
// `quill-delta`, just that it's an instance of a class named `Delta`. We | ||
// define `forEach()` because that's the method that gets used to induce | ||
// an error which helps produce an informative message. | ||
class Delta { | ||
forEach(func) { | ||
func('x'); | ||
} | ||
} | ||
const wrongDelta = new Delta(); | ||
wrongDelta.ops = []; | ||
assert.throws(() => { BodyDelta.fromQuillForm(wrongDelta); }, /Divergent/); | ||
}); | ||
}); | ||
@@ -179,3 +202,3 @@ | ||
it('should produce the new doc when composing the orig doc with the diff', () => { | ||
it('should produce the new document when composing the orig document with the diff', () => { | ||
const diff = origDoc.diff(newDoc); | ||
@@ -182,0 +205,0 @@ const result = origDoc.compose(diff, true); |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -8,2 +8,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import { inspect } from 'util'; | ||
import GraphemeSplitter from 'grapheme-splitter'; | ||
@@ -13,2 +14,7 @@ import { BodyOp } from '@bayou/doc-common'; | ||
// Instance of the grapheme splitter library | ||
// used to properly count the length of | ||
// special characters, such as emojis. | ||
const splitter = new GraphemeSplitter(); | ||
describe('@bayou/doc-common/BodyOp', () => { | ||
@@ -283,2 +289,71 @@ describe('fromQuillForm()', () => { | ||
}); | ||
describe('getLength()', () => { | ||
const PLAIN_TEXT = 'plain text'; | ||
const PLAIN_TEXT_LENGTH = PLAIN_TEXT.length; | ||
const EMOJI_TEXT = '😀 smile!'; | ||
const EMOJI_TEXT_LENGTH = splitter.countGraphemes(EMOJI_TEXT); | ||
const EMBED_OP = BodyOp.op_embed('x', 1); | ||
const PLAIN_TEXT_OP = BodyOp.op_text(PLAIN_TEXT); | ||
const EMOJI_TEXT_OP = BodyOp.op_text(EMOJI_TEXT); | ||
describe('text op', () => { | ||
describe('when `textOnly` is true', () => { | ||
it('should return length of plaintext', () => { | ||
const opLength = PLAIN_TEXT_OP.getLength(true); | ||
assert.strictEqual(opLength, PLAIN_TEXT_LENGTH); | ||
}); | ||
}); | ||
describe('when `textOnly` is false', () => { | ||
it('should return length of plaintext', () => { | ||
const opLength = PLAIN_TEXT_OP.getLength(false); | ||
assert.strictEqual(opLength, PLAIN_TEXT_LENGTH); | ||
}); | ||
}); | ||
describe('when `textOnly` is not set', () => { | ||
it('should return length of plaintext', () => { | ||
const opLength = PLAIN_TEXT_OP.getLength(); | ||
assert.strictEqual(opLength, PLAIN_TEXT_LENGTH); | ||
}); | ||
it('should return length of text with special chars', () => { | ||
const opLength = EMOJI_TEXT_OP.getLength(); | ||
assert.strictEqual(opLength, EMOJI_TEXT_LENGTH); | ||
}); | ||
}); | ||
}); | ||
describe('embed op', () => { | ||
describe('when `textOnly` is true', () => { | ||
it('should return 0', () => { | ||
const opLength = EMBED_OP.getLength(true); | ||
assert.strictEqual(opLength, 0); | ||
}); | ||
}); | ||
describe('when `textOnly` is false', () => { | ||
it('should return 1', () => { | ||
const opLength = EMBED_OP.getLength(false); | ||
assert.strictEqual(opLength, 1); | ||
}); | ||
}); | ||
describe('when `textOnly` is not set', () => { | ||
it('should return 1', () => { | ||
const opLength = EMBED_OP.getLength(); | ||
assert.strictEqual(opLength, 1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -13,15 +13,16 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
* | ||
* @param {string} sessionId Session ID. | ||
* @param {string} id Caret ID. | ||
* @param {Int} index Start caret position. | ||
* @param {Int} length Selection length. | ||
* @param {string} color Highlight color. | ||
* @param {string} authorId Author ID. | ||
* @returns {Caret} Appropriately-constructed caret. | ||
*/ | ||
function newCaret(sessionId, index, length, color) { | ||
return new Caret(sessionId, { index, length, color }); | ||
function newCaret(id, index, length, color, authorId) { | ||
return new Caret(id, { index, length, color, authorId }); | ||
} | ||
const caret1 = newCaret('session-1', 1, 0, '#111111'); | ||
const caret2 = newCaret('session-2', 2, 6, '#222222'); | ||
const caret3 = newCaret('session-3', 3, 99, '#333333'); | ||
const caret1 = newCaret('cr-11111', 1, 0, '#111111', 'author-1'); | ||
const caret2 = newCaret('cr-22222', 2, 6, '#222222', 'author-2'); | ||
const caret3 = newCaret('cr-33333', 3, 99, '#333333', 'third-author'); | ||
@@ -43,4 +44,11 @@ describe('@bayou/doc-common/Caret', () => { | ||
it('should update `authorId` given the appropriate op', () => { | ||
const op = CaretOp.op_setField(caret1.id, 'authorId', 'boop'); | ||
const result = caret1.compose(new CaretDelta([op])); | ||
assert.strictEqual(result.authorId, 'boop'); | ||
}); | ||
it('should update `index` given the appropriate op', () => { | ||
const op = CaretOp.op_setField(caret1.sessionId, 'index', 99999); | ||
const op = CaretOp.op_setField(caret1.id, 'index', 99999); | ||
const result = caret1.compose(new CaretDelta([op])); | ||
@@ -52,3 +60,3 @@ | ||
it('should update `length` given the appropriate op', () => { | ||
const op = CaretOp.op_setField(caret1.sessionId, 'length', 99999); | ||
const op = CaretOp.op_setField(caret1.id, 'length', 99999); | ||
const result = caret1.compose(new CaretDelta([op])); | ||
@@ -60,3 +68,3 @@ | ||
it('should update `color` given the appropriate op', () => { | ||
const op = CaretOp.op_setField(caret1.sessionId, 'color', '#aabbcc'); | ||
const op = CaretOp.op_setField(caret1.id, 'color', '#aabbcc'); | ||
const result = caret1.compose(new CaretDelta([op])); | ||
@@ -68,3 +76,3 @@ | ||
it('should update `revNum` given the appropriate op', () => { | ||
const op = CaretOp.op_setField(caret1.sessionId, 'revNum', 12345); | ||
const op = CaretOp.op_setField(caret1.id, 'revNum', 12345); | ||
const result = caret1.compose(new CaretDelta([op])); | ||
@@ -75,4 +83,4 @@ | ||
it('should refuse to compose if given a non-matching session ID', () => { | ||
const op = CaretOp.op_setField(caret2.sessionId, 'index', 55); | ||
it('should refuse to compose if given a non-matching caret ID', () => { | ||
const op = CaretOp.op_setField(caret2.id, 'index', 55); | ||
@@ -91,3 +99,3 @@ assert.throws(() => { caret1.compose(new CaretDelta([op])); }); | ||
it('should refuse to diff if given a non-matching session ID', () => { | ||
it('should refuse to diff if given a non-matching caret ID', () => { | ||
assert.throws(() => { caret1.diff(caret2); }); | ||
@@ -98,3 +106,3 @@ }); | ||
const older = caret1; | ||
const op = CaretOp.op_setField(older.sessionId, 'index', 99999); | ||
const op = CaretOp.op_setField(older.id, 'index', 99999); | ||
const newer = older.compose(new CaretDelta([op])); | ||
@@ -110,3 +118,3 @@ const diffOps = older.diff(newer).ops; | ||
it('should produce an empty diff when passed itself', () => { | ||
const result = caret1.diffFields(caret1, 'florp'); | ||
const result = caret1.diffFields(caret1, 'cr-florp'); | ||
@@ -117,4 +125,4 @@ assert.instanceOf(result, CaretDelta); | ||
it('should diff fields even if given a non-matching session ID', () => { | ||
assert.doesNotThrow(() => { caret1.diffFields(caret2, 'florp'); }); | ||
it('should diff fields even if given a non-matching caret ID', () => { | ||
assert.doesNotThrow(() => { caret1.diffFields(caret2, 'cr-florp'); }); | ||
}); | ||
@@ -124,5 +132,5 @@ | ||
const older = caret1; | ||
const op = CaretOp.op_setField(older.sessionId, 'index', 99999); | ||
const op = CaretOp.op_setField(older.id, 'index', 99999); | ||
const newer = older.compose(new CaretDelta([op])); | ||
const diffOps = older.diffFields(newer, older.sessionId).ops; | ||
const diffOps = older.diffFields(newer, older.id).ops; | ||
@@ -135,5 +143,5 @@ assert.strictEqual(diffOps.length, 1); | ||
const older = caret1; | ||
const op = CaretOp.op_setField(older.sessionId, 'length', 99999); | ||
const op = CaretOp.op_setField(older.id, 'length', 99999); | ||
const newer = older.compose(new CaretDelta([op])); | ||
const diffOps = older.diffFields(newer, older.sessionId).ops; | ||
const diffOps = older.diffFields(newer, older.id).ops; | ||
@@ -146,5 +154,5 @@ assert.strictEqual(diffOps.length, 1); | ||
const older = caret1; | ||
const op = CaretOp.op_setField(older.sessionId, 'color', '#abcdef'); | ||
const op = CaretOp.op_setField(older.id, 'color', '#abcdef'); | ||
const newer = older.compose(new CaretDelta([op])); | ||
const diffOps = older.diffFields(newer, older.sessionId).ops; | ||
const diffOps = older.diffFields(newer, older.id).ops; | ||
@@ -169,5 +177,5 @@ assert.strictEqual(diffOps.length, 1); | ||
it('should return `false` when session IDs differ', () => { | ||
const c1 = newCaret('x', 1, 2, '#000011'); | ||
const c2 = newCaret('y', 1, 2, '#000011'); | ||
it('should return `false` when caret IDs differ', () => { | ||
const c1 = newCaret('cr-xxxxx', 1, 2, '#000011', 'some-author'); | ||
const c2 = newCaret('cr-yyyyy', 1, 2, '#000011', 'some-author'); | ||
assert.isFalse(c1.equals(c2)); | ||
@@ -180,17 +188,21 @@ }); | ||
op = CaretOp.op_setField(c1.sessionId, 'index', 99999); | ||
op = CaretOp.op_setField(c1.id, 'index', 99999); | ||
c2 = c1.compose(new CaretDelta([op])); | ||
assert.isFalse(c1.equals(c2)); | ||
op = CaretOp.op_setField(c1.sessionId, 'length', 99999); | ||
op = CaretOp.op_setField(c1.id, 'length', 99999); | ||
c2 = c1.compose(new CaretDelta([op])); | ||
assert.isFalse(c1.equals(c2)); | ||
op = CaretOp.op_setField(c1.sessionId, 'color', '#999999'); | ||
op = CaretOp.op_setField(c1.id, 'color', '#999999'); | ||
c2 = c1.compose(new CaretDelta([op])); | ||
assert.isFalse(c1.equals(c2)); | ||
op = CaretOp.op_setField(c1.id, 'authorId', 'zagnut'); | ||
c2 = c1.compose(new CaretDelta([op])); | ||
assert.isFalse(c1.equals(c2)); | ||
}); | ||
it('should return `false` when passed a non-caret', () => { | ||
const caret = newCaret('x', 1, 2, '#000011'); | ||
const caret = newCaret('cr-florp', 1, 2, '#000011', 'blorp'); | ||
@@ -197,0 +209,0 @@ assert.isFalse(caret.equals(undefined)); |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -60,8 +60,8 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
it('should not include session ends when `wantDocument` is `true`', () => { | ||
const op1 = CaretOp.op_beginSession(new Caret('aaa')); | ||
const op2 = CaretOp.op_beginSession(new Caret('bbb')); | ||
const op3 = CaretOp.op_beginSession(new Caret('ccc')); | ||
const op4 = CaretOp.op_endSession('bbb'); | ||
const op5 = CaretOp.op_endSession('ddd'); | ||
it('should not include `delete` ops when `wantDocument` is `true`', () => { | ||
const op1 = CaretOp.op_add(new Caret('cr-aaaaa', { authorId: 'xyz' })); | ||
const op2 = CaretOp.op_add(new Caret('cr-bbbbb', { authorId: 'xyz' })); | ||
const op3 = CaretOp.op_add(new Caret('cr-ccccc', { authorId: 'xyz' })); | ||
const op4 = CaretOp.op_delete('cr-bbbbb'); | ||
const op5 = CaretOp.op_delete('cr-ddddd'); | ||
const d1 = new CaretDelta([op1, op2]); | ||
@@ -74,5 +74,5 @@ const d2 = new CaretDelta([op3, op4, op5]); | ||
describe('`endSession` preceded by anything for that session', () => { | ||
it('should result in just the `endSession`', () => { | ||
const endOp = CaretOp.op_endSession('session1'); | ||
describe('`delete` preceded by anything for that caret', () => { | ||
it('should result in just the `delete`', () => { | ||
const endOp = CaretOp.op_delete('cr-sessi'); | ||
@@ -98,3 +98,3 @@ test( | ||
test( | ||
[CaretOp.op_beginSession(new Caret('session1'))], | ||
[CaretOp.op_add(new Caret('cr-sessi', { authorId: 'xyz' }))], | ||
[endOp], | ||
@@ -105,3 +105,3 @@ [endOp] | ||
test( | ||
[CaretOp.op_setField('session1', 'revNum', 5)], | ||
[CaretOp.op_setField('cr-sessi', 'revNum', 5)], | ||
[endOp], | ||
@@ -113,6 +113,6 @@ [endOp] | ||
describe('`setField` after `endSession`', () => { | ||
it('should result in just the `endSession`', () => { | ||
const endOp = CaretOp.op_endSession('session1'); | ||
const setOp = CaretOp.op_setField('session1', 'revNum', 123); | ||
describe('`setField` after `delete`', () => { | ||
it('should result in just the `delete`', () => { | ||
const endOp = CaretOp.op_delete('cr-sess1'); | ||
const setOp = CaretOp.op_setField('cr-sess1', 'revNum', 123); | ||
@@ -138,3 +138,3 @@ test( | ||
test( | ||
[CaretOp.op_beginSession(new Caret('session1')), endOp], | ||
[CaretOp.op_add(new Caret('cr-sess1', { authorId: 'xyz' })), endOp], | ||
[setOp], | ||
@@ -146,7 +146,7 @@ [endOp] | ||
describe('`setField` after `beginSession`', () => { | ||
it('should result in a modified `beginSession`', () => { | ||
const beginOp = CaretOp.op_beginSession(new Caret('session1')); | ||
const setOp = CaretOp.op_setField('session1', 'revNum', 123); | ||
const resultOp = CaretOp.op_beginSession(new Caret('session1', { revNum: 123 })); | ||
describe('`setField` after `add`', () => { | ||
it('should result in a modified `add`', () => { | ||
const beginOp = CaretOp.op_add(new Caret('cr-sess1', { authorId: 'xyz' })); | ||
const setOp = CaretOp.op_setField('cr-sess1', 'revNum', 123); | ||
const resultOp = CaretOp.op_add(new Caret('cr-sess1', { authorId: 'xyz', revNum: 123 })); | ||
@@ -172,3 +172,3 @@ test( | ||
test( | ||
[beginOp, CaretOp.op_setField('session1', 'revNum', 9999)], | ||
[beginOp, CaretOp.op_setField('cr-sess1', 'revNum', 9999)], | ||
[setOp], | ||
@@ -182,4 +182,4 @@ [resultOp] | ||
it('should drop earlier sets for the same field', () => { | ||
const setOp1 = CaretOp.op_setField('session1', 'revNum', 123); | ||
const setOp2 = CaretOp.op_setField('session1', 'revNum', 999); | ||
const setOp1 = CaretOp.op_setField('cr-sess1', 'revNum', 123); | ||
const setOp2 = CaretOp.op_setField('cr-sess1', 'revNum', 999); | ||
@@ -186,0 +186,0 @@ test( |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -8,3 +8,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import { Caret, CaretChange, CaretDelta, CaretOp, CaretSnapshot } from '@bayou/doc-common'; | ||
import { Caret, CaretChange, CaretDelta, CaretId, CaretOp, CaretSnapshot } from '@bayou/doc-common'; | ||
@@ -14,33 +14,35 @@ /** | ||
* | ||
* @param {string} sessionId Session ID. | ||
* @param {string} id Caret ID. | ||
* @param {Int} index Start caret position. | ||
* @param {Int} length Selection length. | ||
* @param {string} color Highlight color. | ||
* @param {string} authorId Author ID. | ||
* @returns {Caret} Appropriately-constructed caret. | ||
*/ | ||
function newCaret(sessionId, index, length, color) { | ||
return new Caret(sessionId, { index, length, color }); | ||
function newCaret(id, index, length, color, authorId) { | ||
return new Caret(id, { index, length, color, authorId }); | ||
} | ||
/** | ||
* Convenient `op_beginSession` constructor, which takes positional parameters | ||
* Convenient `op_add` constructor, which takes positional parameters | ||
* for the caret fields. | ||
* | ||
* @param {string} sessionId Session ID. | ||
* @param {string} id Caret ID. | ||
* @param {Int} index Start caret position. | ||
* @param {Int} length Selection length. | ||
* @param {string} color Highlight color. | ||
* @param {string} authorId Author ID. | ||
* @returns {Caret} Appropriately-constructed caret. | ||
*/ | ||
function newCaretOp(sessionId, index, length, color) { | ||
return CaretOp.op_beginSession(newCaret(sessionId, index, length, color)); | ||
function newCaretOp(id, index, length, color, authorId) { | ||
return CaretOp.op_add(newCaret(id, index, length, color, authorId)); | ||
} | ||
const caret1 = newCaret('session_1', 1, 0, '#111111'); | ||
const caret2 = newCaret('session_2', 2, 6, '#222222'); | ||
const caret3 = newCaret('session_3', 3, 99, '#333333'); | ||
const caret1 = newCaret('cr-11111', 1, 0, '#111111', 'aa'); | ||
const caret2 = newCaret('cr-22222', 2, 6, '#222222', 'bb'); | ||
const caret3 = newCaret('cr-33333', 3, 99, '#333333', 'cc'); | ||
const op1 = CaretOp.op_beginSession(caret1); | ||
const op2 = CaretOp.op_beginSession(caret2); | ||
const op3 = CaretOp.op_beginSession(caret3); | ||
const op1 = CaretOp.op_add(caret1); | ||
const op2 = CaretOp.op_add(caret2); | ||
const op3 = CaretOp.op_add(caret3); | ||
@@ -105,4 +107,4 @@ describe('@bayou/doc-common/CaretSnapshot', () => { | ||
test([op1, 'florp', op2]); | ||
test([CaretOp.op_endSession('x')]); // Session ends aren't allowed. | ||
test([CaretOp.op_setField('x', 'revNum', 1)]); // Individual field sets aren't allowed. | ||
test([CaretOp.op_delete('cr-xxxxx')]); // `delete`s aren't allowed. | ||
test([CaretOp.op_setField('cr-xxxxx', 'revNum', 1)]); // Individual field sets aren't allowed. | ||
test([op1, op1]); // Duplicates aren't allowed. | ||
@@ -117,11 +119,11 @@ }); | ||
// Session ends aren't allowed. | ||
test([CaretOp.op_endSession('x')]); | ||
test([op1, CaretOp.op_endSession('x')]); | ||
test([op1, CaretOp.op_endSession(caret1.sessionId)]); | ||
// `delete` ops aren't allowed. | ||
test([CaretOp.op_delete('cr-xxxxx')]); | ||
test([op1, CaretOp.op_delete('cr-xxxxx')]); | ||
test([op1, CaretOp.op_delete(caret1.id)]); | ||
// Individual field sets aren't allowed. | ||
test([CaretOp.op_setField('x', 'revNum', 1)]); | ||
test([op1, CaretOp.op_setField('x', 'revNum', 1)]); | ||
test([op1, CaretOp.op_setField(caret1.sessionId, 'revNum', 1)]); | ||
test([CaretOp.op_setField('cr-xxxxx', 'revNum', 1)]); | ||
test([op1, CaretOp.op_setField('cr-xxxxx', 'revNum', 1)]); | ||
test([op1, CaretOp.op_setField(caret1.id, 'revNum', 1)]); | ||
@@ -188,3 +190,3 @@ // Duplicates aren't allowed. | ||
const expected = new CaretSnapshot(1, [op1]); | ||
const change = new CaretChange(1, [CaretOp.op_beginSession(caret1)]); | ||
const change = new CaretChange(1, [CaretOp.op_add(caret1)]); | ||
const result = snap.compose(change); | ||
@@ -197,3 +199,3 @@ | ||
const snap = new CaretSnapshot(1, [op1]); | ||
const change = new CaretChange(1, [CaretOp.op_setField('florp', 'index', 1)]); | ||
const change = new CaretChange(1, [CaretOp.op_setField('cr-florp', 'index', 1)]); | ||
@@ -204,7 +206,7 @@ assert.throws(() => { snap.compose(change); }); | ||
it('should update a pre-existing caret given an appropriate op', () => { | ||
const c1 = newCaretOp('foo', 1, 2, '#333333'); | ||
const c2 = newCaretOp('foo', 3, 2, '#333333'); | ||
const c1 = newCaretOp('cr-foooo', 1, 2, '#333333', 'dd'); | ||
const c2 = newCaretOp('cr-foooo', 3, 2, '#333333', 'dd'); | ||
const snap = new CaretSnapshot(1, [op1, c1]); | ||
const expected = new CaretSnapshot(1, [op1, c2]); | ||
const op = CaretOp.op_setField('foo', 'index', 3); | ||
const op = CaretOp.op_setField('cr-foooo', 'index', 3); | ||
const result = snap.compose(new CaretChange(1, [op])); | ||
@@ -218,3 +220,3 @@ | ||
const expected = new CaretSnapshot(1, [op2]); | ||
const result = snap.compose(new CaretChange(1, [CaretOp.op_endSession(caret1.sessionId)])); | ||
const result = snap.compose(new CaretChange(1, [CaretOp.op_delete(caret1.id)])); | ||
@@ -269,5 +271,5 @@ assert.isTrue(result.equals(expected)); | ||
it('should result in a caret update if that in fact happens', () => { | ||
const c1 = newCaretOp('florp', 1, 3, '#444444'); | ||
const c2 = newCaretOp('florp', 2, 4, '#555555'); | ||
const c3 = newCaretOp('florp', 3, 5, '#666666'); | ||
const c1 = newCaretOp('cr-florp', 1, 3, '#444444', 'ff'); | ||
const c2 = newCaretOp('cr-florp', 2, 4, '#555555', 'gg'); | ||
const c3 = newCaretOp('cr-florp', 3, 5, '#666666', 'hh'); | ||
const snap1 = new CaretSnapshot(1, [c1]); | ||
@@ -301,9 +303,9 @@ const snap2 = new CaretSnapshot(1, [c2]); | ||
const caret = op.props.caret; | ||
expectMap.set(caret.sessionId, caret); | ||
expectMap.set(caret.id, caret); | ||
} | ||
const snap = new CaretSnapshot(1, ops); | ||
for (const [sessionId, caret] of snap.entries()) { | ||
assert.strictEqual(caret, expectMap.get(sessionId)); | ||
expectMap.delete(sessionId); | ||
for (const [caretId, caret] of snap.entries()) { | ||
assert.strictEqual(caret, expectMap.get(caretId)); | ||
expectMap.delete(caretId); | ||
} | ||
@@ -358,6 +360,6 @@ | ||
it('should return `true` when equal carets are not also `===`', () => { | ||
const c1a = newCaretOp('florp', 2, 3, '#444444'); | ||
const c1b = newCaretOp('florp', 2, 3, '#444444'); | ||
const c2a = newCaretOp('like', 3, 0, '#dbdbdb'); | ||
const c2b = newCaretOp('like', 3, 0, '#dbdbdb'); | ||
const c1a = newCaretOp('cr-florp', 2, 3, '#444444', 'ab'); | ||
const c1b = newCaretOp('cr-florp', 2, 3, '#444444', 'ab'); | ||
const c2a = newCaretOp('cr-like0', 3, 0, '#dbdbdb', 'cd'); | ||
const c2b = newCaretOp('cr-like0', 3, 0, '#dbdbdb', 'cd'); | ||
@@ -438,17 +440,17 @@ const snap1 = new CaretSnapshot(1, [c1a, c2a]); | ||
describe('get()', () => { | ||
it('should return the caret associated with an existing session', () => { | ||
it('should return the caret associated with an existing ID', () => { | ||
const snap = new CaretSnapshot(999, [op1, op2, op3]); | ||
assert.strictEqual(snap.get(caret1.sessionId), caret1); | ||
assert.strictEqual(snap.get(caret2.sessionId), caret2); | ||
assert.strictEqual(snap.get(caret3.sessionId), caret3); | ||
assert.strictEqual(snap.get(caret1.id), caret1); | ||
assert.strictEqual(snap.get(caret2.id), caret2); | ||
assert.strictEqual(snap.get(caret3.id), caret3); | ||
}); | ||
it('should throw an error when given a session ID that is not in the snapshot', () => { | ||
it('should throw an error when given an ID that is not in the snapshot', () => { | ||
const snap = new CaretSnapshot(999, [op1, op3]); | ||
assert.throws(() => { snap.get(caret2.sessionId); }); | ||
assert.throws(() => { snap.get(caret2.id); }); | ||
}); | ||
it('should throw an error if given an invalid session ID', () => { | ||
it('should throw an error if given an invalid ID', () => { | ||
const snap = new CaretSnapshot(999, []); | ||
@@ -463,17 +465,17 @@ | ||
describe('getOrNull()', () => { | ||
it('should return the caret associated with an existing session', () => { | ||
it('should return the caret associated with an existing ID', () => { | ||
const snap = new CaretSnapshot(999, [op1, op2, op3]); | ||
assert.strictEqual(snap.getOrNull(caret1.sessionId), caret1); | ||
assert.strictEqual(snap.getOrNull(caret2.sessionId), caret2); | ||
assert.strictEqual(snap.getOrNull(caret3.sessionId), caret3); | ||
assert.strictEqual(snap.getOrNull(caret1.id), caret1); | ||
assert.strictEqual(snap.getOrNull(caret2.id), caret2); | ||
assert.strictEqual(snap.getOrNull(caret3.id), caret3); | ||
}); | ||
it('should return `null` when given a session ID that is not in the snapshot', () => { | ||
it('should return `null` when given an ID that is not in the snapshot', () => { | ||
const snap = new CaretSnapshot(999, [op1, op3]); | ||
assert.isNull(snap.getOrNull(caret2.sessionId)); | ||
assert.isNull(snap.getOrNull(caret2.id)); | ||
}); | ||
it('should throw an error if given an invalid session ID', () => { | ||
it('should throw an error if given an invalid ID', () => { | ||
const snap = new CaretSnapshot(999, []); | ||
@@ -488,17 +490,17 @@ | ||
describe('has()', () => { | ||
it('should return `true` when given a session ID for an existing session', () => { | ||
it('should return `true` when given an ID for an existing caret', () => { | ||
const snap = new CaretSnapshot(999, [op1, op2, op3]); | ||
assert.isTrue(snap.has(caret1.sessionId)); | ||
assert.isTrue(snap.has(caret2.sessionId)); | ||
assert.isTrue(snap.has(caret3.sessionId)); | ||
assert.isTrue(snap.has(caret1.id)); | ||
assert.isTrue(snap.has(caret2.id)); | ||
assert.isTrue(snap.has(caret3.id)); | ||
}); | ||
it('should return `false` when given a session ID that is not in the snapshot', () => { | ||
it('should return `false` when given an ID that is not in the snapshot', () => { | ||
const snap = new CaretSnapshot(999, [op1, op3]); | ||
assert.isFalse(snap.has(caret2.sessionId)); | ||
assert.isFalse(snap.has(caret2.id)); | ||
}); | ||
it('should throw an error if given an invalid session ID', () => { | ||
it('should throw an error if given an invalid ID', () => { | ||
const snap = new CaretSnapshot(999, []); | ||
@@ -512,2 +514,35 @@ | ||
describe('randomUnusedId()', () => { | ||
it('should return a string for which `CaretId.isInstance()` is `true`', () => { | ||
const snap = new CaretSnapshot(999, [op1, op2, op3]); | ||
const id = snap.randomUnusedId(); | ||
assert.isTrue(CaretId.isInstance(id)); | ||
}); | ||
it('should return an ID that is not used', () => { | ||
// What we're doing here is mocking out `CaretSnapshot.has()` to lie about | ||
// the IDs in the instance N times, so that we can infer that the method | ||
// under test actually retries. | ||
const snap = new CaretSnapshot(999, []); | ||
let retries = 10; | ||
let gotId = null; | ||
const mocked = Object.create(snap); | ||
mocked.has = (id) => { | ||
if (retries === 0) { | ||
gotId = id; | ||
return false; | ||
} else { | ||
retries--; | ||
return true; | ||
} | ||
}; | ||
const result = mocked.randomUnusedId(); | ||
assert.strictEqual(result, gotId); | ||
assert.strictEqual(retries, 0); | ||
}); | ||
}); | ||
describe('withCaret()', () => { | ||
@@ -532,3 +567,3 @@ it('should return `this` if the exact caret is already in the snapshot', () => { | ||
const modCaret = new Caret(caret1, { index: 321 }); | ||
const modOp = CaretOp.op_beginSession(modCaret); | ||
const modOp = CaretOp.op_add(modCaret); | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
@@ -588,39 +623,57 @@ const expected = new CaretSnapshot(1, [modOp, op2]); | ||
describe('withoutCaret()', () => { | ||
it('should return `this` if there is no matching session', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
describe('valid `Caret` argument', () => { | ||
it('should return `this` if there is no matching caret', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
assert.strictEqual(snap.withoutCaret(caret2), snap); | ||
assert.strictEqual(snap.withoutCaret(caret3), snap); | ||
}); | ||
assert.strictEqual(snap.withoutCaret(caret2), snap); | ||
assert.strictEqual(snap.withoutCaret(caret3), snap); | ||
}); | ||
it('should return an appropriately-constructed instance if there is a matching session', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
it('should return an appropriately-constructed instance if there is a matching caret', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
assert.isTrue(snap.withoutCaret(caret1).equals(expected)); | ||
}); | ||
assert.isTrue(snap.withoutCaret(caret1).equals(expected)); | ||
}); | ||
it('should only pay attention to the session ID of the given caret', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
const modCaret = new Caret(caret1, { revNum: 999999, index: 99 }); | ||
it('should only pay attention to the ID of the given caret', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
const modCaret = new Caret(caret1, { revNum: 999999, index: 99 }); | ||
assert.isTrue(snap.withoutCaret(modCaret).equals(expected)); | ||
assert.isTrue(snap.withoutCaret(modCaret).equals(expected)); | ||
}); | ||
}); | ||
}); | ||
describe('withoutSession()', () => { | ||
it('should return `this` if there is no matching session', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
describe('valid ID argument', () => { | ||
it('should return `this` if there is no matching caret', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
assert.strictEqual(snap.withoutSession('blort_is_not_a_session'), snap); | ||
assert.strictEqual(snap.withoutCaret('cr-not00'), snap); | ||
}); | ||
it('should return an appropriately-constructed instance if there is a matching caret', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
assert.isTrue(snap.withoutCaret(caret1.id).equals(expected)); | ||
}); | ||
}); | ||
it('should return an appropriately-constructed instance if there is a matching session', () => { | ||
const snap = new CaretSnapshot(1, [op1, op2]); | ||
const expected = new CaretSnapshot(1, [op2]); | ||
describe('invalid argument', () => { | ||
it('should reject invalid ID strings', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
assert.throws(() => snap.withoutCaret('')); | ||
assert.throws(() => snap.withoutCaret('ZORCH_SPLAT')); | ||
}); | ||
assert.isTrue(snap.withoutSession(caret1.sessionId).equals(expected)); | ||
it('should reject arguments that are neither strings nor `Caret`s', () => { | ||
const snap = new CaretSnapshot(1, [op1]); | ||
assert.throws(() => snap.withoutCaret(undefined)); | ||
assert.throws(() => snap.withoutCaret(null)); | ||
assert.throws(() => snap.withoutCaret(123)); | ||
assert.throws(() => snap.withoutCaret([])); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -22,2 +22,3 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> | ||
import PropertySnapshot from './PropertySnapshot'; | ||
import SessionInfo from './SessionInfo'; | ||
@@ -40,3 +41,3 @@ /** | ||
static get SCHEMA_VERSION() { | ||
return '2018-001'; | ||
return '2018-004'; | ||
} | ||
@@ -66,3 +67,4 @@ | ||
registry.registerClass(PropertySnapshot); | ||
registry.registerClass(SessionInfo); | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
// Copyright 2016-2018 the Bayou Authors (Dan Bornstein et alia). | ||
// Copyright 2016-2019 the Bayou Authors (Dan Bornstein et alia). | ||
// Licensed AS IS and WITHOUT WARRANTY under the Apache License, | ||
@@ -3,0 +3,0 @@ // Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
194417
33
5119
1
5
8
+ Added@bayou/api-common@1.2.4
+ Addedgrapheme-splitter@^1.0.2
+ Added@bayou/api-common@1.2.4(transitive)
+ Added@bayou/codec@1.2.4(transitive)
+ Added@bayou/config-common@1.2.4(transitive)
+ Added@bayou/injecty@1.2.4(transitive)
+ Added@bayou/ot-common@1.2.4(transitive)
+ Added@bayou/see-all@1.2.4(transitive)
+ Added@bayou/typecheck@1.2.4(transitive)
+ Added@bayou/util-common@1.2.4(transitive)
+ Added@bayou/util-core@1.2.4(transitive)
+ Addedgrapheme-splitter@1.0.4(transitive)
- Removed@bayou/codec@1.0.6(transitive)
- Removed@bayou/config-common@1.0.6(transitive)
- Removed@bayou/injecty@1.0.6(transitive)
- Removed@bayou/ot-common@1.0.6(transitive)
- Removed@bayou/see-all@1.0.6(transitive)
- Removed@bayou/typecheck@1.0.6(transitive)
- Removed@bayou/util-common@1.0.6(transitive)
- Removed@bayou/util-core@1.0.6(transitive)
Updated@bayou/codec@1.2.4
Updated@bayou/config-common@1.2.4
Updated@bayou/ot-common@1.2.4
Updated@bayou/see-all@1.2.4
Updated@bayou/typecheck@1.2.4
Updated@bayou/util-common@1.2.4