From 05a5d77e7649d2ba1006b34cca8b30a1c977a937 Mon Sep 17 00:00:00 2001 From: mweidner037 <17693586+mweidner037@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:20:20 -0400 Subject: [PATCH] Update to 1.0 (#9) * update to 1.0 and new @list-positions/formatting name * move repo to list-positions-demos * revise readme * remove order restriction for TimestampFormattingSavedStates * RichList -> RichText (rename only) * changes to match RichList -> RichText * fix replicache-quill errors * update typescript * test all --- README.md | 6 +- electricsql-quill/README.md | 2 +- .../db/migrations/01-create_docs.sql | 2 +- electricsql-quill/package-lock.json | 42 ++++----- electricsql-quill/package.json | 4 +- electricsql-quill/src/quill/ElectricQuill.tsx | 2 +- electricsql-quill/src/quill/quill_wrapper.ts | 92 +++++++++---------- replicache-quill/README.md | 4 +- replicache-quill/client/index.html | 2 +- replicache-quill/client/package.json | 6 +- replicache-quill/client/src/index.ts | 23 +++-- replicache-quill/client/src/quill_wrapper.ts | 80 ++++++++-------- replicache-quill/package-lock.json | 72 +++++++-------- replicache-quill/package.json | 2 +- replicache-quill/server/package.json | 6 +- replicache-quill/shared/package.json | 6 +- replicache-quill/shared/src/mutators.ts | 7 +- replicache-quill/shared/src/rich_text.ts | 9 +- suggested-changes/package-lock.json | 42 ++++----- suggested-changes/package.json | 4 +- suggested-changes/src/common/block_text.ts | 2 +- suggested-changes/src/common/messages.ts | 2 +- .../src/server/rich_text_server.ts | 3 +- suggested-changes/src/site/index.html | 2 +- .../src/site/prosemirror_wrapper.ts | 2 +- suggested-changes/src/site/suggestion.ts | 3 +- triplit-quill/README.md | 2 +- triplit-quill/index.html | 2 +- triplit-quill/package-lock.json | 42 ++++----- triplit-quill/package.json | 4 +- triplit-quill/src/main.ts | 10 +- triplit-quill/src/quill_wrapper.ts | 74 +++++++-------- websocket-prosemirror-blocks/README.md | 2 +- .../package-lock.json | 42 ++++----- websocket-prosemirror-blocks/package.json | 4 +- .../src/common/block_text.ts | 2 +- .../src/common/messages.ts | 2 +- .../src/server/rich_text_server.ts | 3 +- .../src/site/index.html | 2 +- .../src/site/prosemirror_wrapper.ts | 2 +- websocket-prosemirror-log/package-lock.json | 28 +++--- websocket-prosemirror-log/package.json | 4 +- .../src/common/messages.ts | 1 - websocket-prosemirror-log/src/site/index.html | 2 +- websocket-quill/README.md | 2 +- websocket-quill/package-lock.json | 58 ++++++------ websocket-quill/package.json | 10 +- websocket-quill/src/common/messages.ts | 13 +-- .../src/server/rich_text_server.ts | 24 +++-- websocket-quill/src/site/index.html | 2 +- websocket-quill/src/site/main.ts | 2 +- websocket-quill/src/site/quill_wrapper.ts | 62 ++++++------- 52 files changed, 403 insertions(+), 425 deletions(-) diff --git a/README.md b/README.md index 18bf323..73d96c9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# List Demos +# list-positions Demos -Demos using the [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme) libraries. +Demos using the [list-positions](https://github.com/mweidner037/list-positions#readme) library and its companion library ([@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme).) -Note: These demos are prototypes. I am working on "bindings" for specific rich-text editors which aim to be more thoroughly tested and to support additional features (e.g., embedded media). +Note: These demos are prototypes. I am working on "bindings" for specific rich-text editors, which aim to be more thoroughly tested and to support additional features (e.g., embedded media). - [`websocket-quill/`](./websocket-quill#readme): Basic collaborative rich-text editor using a WebSocket server and Quill. - [`websocket-prosemirror-log/`](./websocket-prosemirror-log#readme): Basic collaborative rich-text editor using a WebSocket server and ProseMirror, with support for arbitrary schemas, using a log of mutations. diff --git a/electricsql-quill/README.md b/electricsql-quill/README.md index 0ef4994..106fc5a 100644 --- a/electricsql-quill/README.md +++ b/electricsql-quill/README.md @@ -1,6 +1,6 @@ # ElectricSQL-Quill -Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme), the [ElectricSQL](https://electric-sql.com/) database sync service, and [Quill](https://quilljs.com/). +Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme), the [ElectricSQL](https://electric-sql.com/) database sync service, and [Quill](https://quilljs.com/). This is a web application using ElectricSQL in the browser with [wa-sqlite](https://electric-sql.com/docs/integrations/drivers/web/wa-sqlite). The editor state is stored in a SQL database with three tables: diff --git a/electricsql-quill/db/migrations/01-create_docs.sql b/electricsql-quill/db/migrations/01-create_docs.sql index 693b471..daf39c4 100644 --- a/electricsql-quill/db/migrations/01-create_docs.sql +++ b/electricsql-quill/db/migrations/01-create_docs.sql @@ -32,7 +32,7 @@ CREATE TABLE char_entries ( ALTER TABLE char_entries ENABLE ELECTRIC; --- Add-only log of TimestampMarks from list-formatting. +-- Add-only log of TimestampMarks from @list-positions/formatting. CREATE TABLE formatting_marks ( -- String encoding of (creatorID, timestamp), used since we need a primary key -- but don't want to waste space on a UUID. diff --git a/electricsql-quill/package-lock.json b/electricsql-quill/package-lock.json index eca9169..3e275e2 100644 --- a/electricsql-quill/package-lock.json +++ b/electricsql-quill/package-lock.json @@ -8,9 +8,9 @@ "name": "electricsql", "version": "0.1.0", "dependencies": { + "@list-positions/formatting": "^1.0.0", "electric-sql": "^0.9.3", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "quill": "^1.3.7", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1318,6 +1318,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@ljharb/through": { "version": "2.3.13", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", @@ -4636,9 +4645,9 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -4646,21 +4655,12 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } - }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -6157,9 +6157,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/split2": { "version": "4.2.0", diff --git a/electricsql-quill/package.json b/electricsql-quill/package.json index a24b88f..9ab40c1 100644 --- a/electricsql-quill/package.json +++ b/electricsql-quill/package.json @@ -20,8 +20,8 @@ }, "dependencies": { "electric-sql": "^0.9.3", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.0", + "list-positions": "^1.0.0", "quill": "^1.3.7", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/electricsql-quill/src/quill/ElectricQuill.tsx b/electricsql-quill/src/quill/ElectricQuill.tsx index a47fd8c..e117ef4 100644 --- a/electricsql-quill/src/quill/ElectricQuill.tsx +++ b/electricsql-quill/src/quill/ElectricQuill.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import { useLiveQuery } from "electric-sql/react"; -import { TimestampMark } from "list-formatting"; +import { TimestampMark } from "@list-positions/formatting"; import { BunchMeta, Position, expandPositions } from "list-positions"; import { useElectric } from "../Loader"; import { QuillWrapper, WrapperOp } from "./quill_wrapper"; diff --git a/electricsql-quill/src/quill/quill_wrapper.ts b/electricsql-quill/src/quill/quill_wrapper.ts index 09aedb9..9adc002 100644 --- a/electricsql-quill/src/quill/quill_wrapper.ts +++ b/electricsql-quill/src/quill/quill_wrapper.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { - FormattedValues, - RichList, - RichListSavedState, + FormattedChars, + RichText, + RichTextSavedState, TimestampMark, sliceFromSpan, -} from "list-formatting"; +} from "@list-positions/formatting"; import { BunchMeta, MAX_POSITION, @@ -50,7 +50,7 @@ export class QuillWrapper { /** * Instead of editing this directly, use the applyOps method. */ - readonly richList: RichList; + readonly richText: RichText; private ourChange = false; @@ -66,13 +66,11 @@ export class QuillWrapper { readonly onLocalOps: (ops: WrapperOp[]) => void, /** * Must end in "\n" to match Quill, even if otherwise empty. - * - * Okay if marks are not in compareMarks order (weaker than RichListSavedState reqs). */ - initialState: RichListSavedState, + initialState: RichTextSavedState, order?: Order ) { - this.richList = new RichList({ expandRules, order }); + this.richText = new RichText({ expandRules, order }); // Setup Quill. this.editor = new Quill(container, { @@ -112,10 +110,10 @@ export class QuillWrapper { [...Object.entries(quillAttrs)].map(quillAttrToFormatting) ); const [startPos, createdBunch, createdMarks] = - this.richList.insertWithFormat( + this.richText.insertWithFormat( deltaOp.index, formattingAttrs, - ...deltaOp.insert + deltaOp.insert ); if (createdBunch) { // Push meta op first to avoid missing BunchMeta deps. @@ -133,13 +131,13 @@ export class QuillWrapper { // Deletion else if (deltaOp.delete) { const toDelete = [ - ...this.richList.list.positions( + ...this.richText.text.positions( deltaOp.index, deltaOp.index + deltaOp.delete ), ]; for (const pos of toDelete) { - this.richList.list.delete(pos); + this.richText.text.delete(pos); wrapperOps.push({ type: "delete", startPos: pos, @@ -152,7 +150,7 @@ export class QuillWrapper { deltaOp.attributes )) { const [key, value] = quillAttrToFormatting([quillKey, quillValue]); - const [mark] = this.richList.format( + const [mark] = this.richText.format( deltaOp.index, deltaOp.index + deltaOp.retain, key, @@ -186,7 +184,7 @@ export class QuillWrapper { allMetas.push(...op.metas); } } - this.richList.order.addMetas(allMetas); + this.richText.order.addMetas(allMetas); // Process the non-"metas" ops. let pendingDelta: DeltaStatic = new Delta(); @@ -200,10 +198,10 @@ export class QuillWrapper { for (let i = 0; i < poss.length; i++) { const pos = poss[i]; const char = op.chars[i]; - if (!this.richList.list.has(pos)) { - this.richList.list.set(pos, char); - const index = this.richList.list.indexOfPosition(pos); - const format = this.richList.formatting.getFormat(pos); + if (!this.richText.text.has(pos)) { + this.richText.text.set(pos, char); + const index = this.richText.text.indexOfPosition(pos); + const format = this.richText.formatting.getFormat(pos); pendingDelta = pendingDelta.compose( new Delta() .retain(index) @@ -216,9 +214,9 @@ export class QuillWrapper { case "delete": // OPT: Apply these in bulk if possible (common case of causally ordered ops). for (const pos of expandPositions(op.startPos, op.count ?? 1)) { - if (this.richList.list.has(pos)) { - const index = this.richList.list.indexOfPosition(pos); - this.richList.list.delete(pos); + if (this.richText.text.has(pos)) { + const index = this.richText.text.indexOfPosition(pos); + this.richText.text.delete(pos); pendingDelta = pendingDelta.compose( new Delta().retain(index).delete(1) ); @@ -227,10 +225,10 @@ export class QuillWrapper { break; case "marks": { for (const mark of op.marks) { - const changes = this.richList.formatting.addMark(mark); + const changes = this.richText.formatting.addMark(mark); for (const change of changes) { const { startIndex, endIndex } = sliceFromSpan( - this.richList.list, + this.richText.text, change.start, change.end ); @@ -268,34 +266,29 @@ export class QuillWrapper { * * Note: Order is not cleared, just appended. */ - load(savedState: RichListSavedState): void { + load(savedState: RichTextSavedState): void { this.ourChange = true; try { // Clear existing state. - this.richList.clear(); + this.richText.clear(); this.editor.setContents(new Delta()); - // Load savedState into richList. - this.richList.order.load(savedState.order); - this.richList.list.load(savedState.list); - // savedState.marks is not a saved state; add directly. - for (const mark of savedState.formatting) { - this.richList.formatting.addMark(mark); - } + // Load savedState into richText. + this.richText.load(savedState); if ( - this.richList.list.length === 0 || - this.richList.list.getAt(this.richList.list.length - 1) !== "\n" + this.richText.text.length === 0 || + this.richText.text.getAt(this.richText.text.length - 1) !== "\n" ) { throw new Error('Bad saved state: must end in "\n" to match Quill'); } // Sync savedState to Quill. this.editor.updateContents( - deltaFromSlices(this.richList.formattedValues()) + deltaFromSlices(this.richText.formattedChars()) ); // Delete Quill's own initial "\n" - the savedState is supposed to end with one. this.editor.updateContents( - new Delta().retain(this.richList.list.length).delete(1) + new Delta().retain(this.richText.text.length).delete(1) ); } finally { this.ourChange = false; @@ -307,8 +300,8 @@ export class QuillWrapper { return quillSel === null ? null : { - start: this.richList.list.cursorAt(quillSel.index), - end: this.richList.list.cursorAt(quillSel.index + quillSel.length), + start: this.richText.text.cursorAt(quillSel.index), + end: this.richText.text.cursorAt(quillSel.index + quillSel.length), }; } @@ -318,8 +311,8 @@ export class QuillWrapper { this.editor.setSelection(0, 0); this.editor.blur(); } else { - const startIndex = this.richList.list.indexOfCursor(sel.start); - const endIndex = this.richList.list.indexOfCursor(sel.end); + const startIndex = this.richText.text.indexOfCursor(sel.start); + const endIndex = this.richText.text.indexOfCursor(sel.end); this.editor.setSelection({ index: startIndex, length: endIndex - startIndex, @@ -336,15 +329,15 @@ export class QuillWrapper { * "\n", to match Quill's initial state. */ static makeInitialState() { - const richList = new RichList(); - const [pos] = richList.order.createPositions( + const richText = new RichText(); + const [pos] = richText.order.createPositions( MIN_POSITION, MAX_POSITION, 1, { bunchID: "INIT" } ); - richList.list.set(pos, "\n"); - return richList.save(); + richText.text.set(pos, "\n"); + return richText.save(); } } @@ -408,13 +401,10 @@ function getRelevantDeltaOperations(delta: DeltaStatic): { return relevantOps; } -function deltaFromSlices(slices: FormattedValues[]) { +function deltaFromSlices(slices: FormattedChars[]) { let delta = new Delta(); - for (const values of slices) { - delta = delta.insert( - values.values.join(""), - formattingToQuillAttr(values.format) - ); + for (const slice of slices) { + delta = delta.insert(slice.chars, formattingToQuillAttr(slice.format)); } return delta; } diff --git a/replicache-quill/README.md b/replicache-quill/README.md index db30449..c97f4a1 100644 --- a/replicache-quill/README.md +++ b/replicache-quill/README.md @@ -1,11 +1,11 @@ # Replicache-Quill -Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme), the [Replicache](https://replicache.dev/) client-side sync framework, and [Quill](https://quilljs.com/). +Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme), the [Replicache](https://replicache.dev/) client-side sync framework, and [Quill](https://quilljs.com/). The editor state is stored in Replicache under two prefixes: - `bunch/` for the `List` state, grouped by bunch. Each entry corresponds to a [bunch](https://github.com/mweidner037/list-positions#bunches) from list-positions. It stores the bunch's [BunchMeta](https://github.com/mweidner037/list-positions#managing-metadata) fields, plus its current values (chars) as an object `{ [innerIndex: number]: string }`. -- `mark/` for the formatting marks. Each entry stores a [TimestampMark](https://github.com/mweidner037/list-formatting#class-timestampformatting) from list-formatting, keyed by an arbitrary unique ID. +- `mark/` for the formatting marks. Each entry stores a [TimestampMark](https://github.com/mweidner037/list-positions-formatting#class-timestampformatting) from @list-positions/formatting, keyed by an arbitrary unique ID. Replicache mutators correspond to the basic rich-text operations: diff --git a/replicache-quill/client/index.html b/replicache-quill/client/index.html index c4b1051..df4ed86 100644 --- a/replicache-quill/client/index.html +++ b/replicache-quill/client/index.html @@ -10,7 +10,7 @@
Info and source code
diff --git a/replicache-quill/client/package.json b/replicache-quill/client/package.json index fe86deb..89d54e3 100644 --- a/replicache-quill/client/package.json +++ b/replicache-quill/client/package.json @@ -18,8 +18,8 @@ "watch": "concurrently --kill-others 'npm run server' 'npm run check-types -- --watch --preserveWatchOutput' 'sleep 3; npm run dev'" }, "dependencies": { - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.1", + "list-positions": "^1.0.0", "nanoid": "^4.0.0", "quill": "^1.3.6", "replicache": ">=14.0.3", @@ -30,7 +30,7 @@ "@rocicorp/prettier-config": "^0.1.1", "@rocicorp/eslint-config": "^0.1.2", "prettier": "^2.2.1", - "typescript": "^4.6.4", + "typescript": "^5.4.5", "vite": "^3.0.7", "concurrently": "^7.4.0" }, diff --git a/replicache-quill/client/src/index.ts b/replicache-quill/client/src/index.ts index 336a1d1..2500cc0 100644 --- a/replicache-quill/client/src/index.ts +++ b/replicache-quill/client/src/index.ts @@ -1,4 +1,4 @@ -import {TimestampMark} from 'list-formatting'; +import {TimestampMark} from '@list-positions/formatting'; import { ExperimentalDiffOperationAdd, ExperimentalDiffOperationChange, @@ -55,27 +55,26 @@ async function init() { // Load initial state from Replicache. - const richList = QuillWrapper.newRichList(); + const richText = QuillWrapper.newRichText(); await r.query(async tx => { const bunches = await allBunches(tx); // First need to load all metas together, to avoid dependency ordering concerns. - richList.order.addMetas(bunches.map(bunch => bunch.meta)); + richText.order.addMetas(bunches.map(bunch => bunch.meta)); // Now load all values. for (const bunch of bunches) { // TODO: In list-positions, provide method to set a whole bunch's values quickly. for (const [indexStr, char] of Object.entries(bunch.values)) { const innerIndex = Number.parseInt(indexStr); - richList.list.set({bunchID: bunch.meta.bunchID, innerIndex}, char); + richText.text.set({bunchID: bunch.meta.bunchID, innerIndex}, char); } } - // Load all marks. They are not necessarily in compareMarks order, - // so call addMarks in a loop instead of load (TODO: subject to change). + // Load all marks. const marks = await allMarks(tx); - for (const mark of marks) richList.formatting.addMark(mark); + richText.formatting.load(marks); }); - const quillWrapper = new QuillWrapper(onLocalOps, richList); + const quillWrapper = new QuillWrapper(onLocalOps, richText); // Send future Quill changes to Replicache. // Use a queue to avoid reordered mutations (since onLocalOps is sync @@ -183,13 +182,17 @@ async function init() { } } else if (diffOp.key.startsWith('mark/')) { switch (diffOp.op) { - case 'add': - const op = diffOp as ExperimentalDiffOperationAdd< + case 'add': { + // ReadonlyJSONValue is supposed to express that the value is deep-readonly. + // Because of https://github.com/microsoft/TypeScript/issues/15300 , though, + // it doesn't work on JSON objects whose type is (or includes) an interface. + const op = diffOp as unknown as ExperimentalDiffOperationAdd< string, TimestampMark >; wrapperOps.push({type: 'marks', marks: [op.newValue]}); break; + } default: console.error('Unexpected op on mark key:', diffOp.op, diffOp.key); } diff --git a/replicache-quill/client/src/quill_wrapper.ts b/replicache-quill/client/src/quill_wrapper.ts index 40adf36..84205e6 100644 --- a/replicache-quill/client/src/quill_wrapper.ts +++ b/replicache-quill/client/src/quill_wrapper.ts @@ -2,11 +2,11 @@ import Quill, {DeltaStatic, Delta as DeltaType} from 'quill'; // Quill CSS. import { - FormattedValues, - RichList, + FormattedChars, + RichText, TimestampMark, sliceFromSpan, -} from 'list-formatting'; +} from '@list-positions/formatting'; import { BunchMeta, MAX_POSITION, @@ -43,7 +43,7 @@ export class QuillWrapper { /** * Instead of editing this directly, use the applyOps method. */ - readonly richList: RichList; + readonly richText: RichText; private ourChange = false; @@ -57,15 +57,15 @@ export class QuillWrapper { */ readonly onLocalOps: (ops: WrapperOp[]) => void, /** - * Must come from newRichList and still have the trailing "\n + * Must come from newRichText and still have the trailing "\n * . */ - richList: RichList, + richText: RichText, ) { - this.richList = richList; + this.richText = richText; if ( - this.richList.list.length === 0 || - this.richList.list.getAt(this.richList.list.length - 1) !== '\n' + this.richText.text.length === 0 || + this.richText.text.getAt(this.richText.text.length - 1) !== '\n' ) { throw new Error('Bad initial state: must end in "\n" to match Quill'); } @@ -88,12 +88,10 @@ export class QuillWrapper { }); // Sync initial state to Quill. - this.editor.updateContents( - deltaFromSlices(this.richList.formattedValues()), - ); + this.editor.updateContents(deltaFromSlices(this.richText.formattedChars())); // Delete Quill's own initial "\n" - the initial state is supposed to end with one. this.editor.updateContents( - new Delta().retain(this.richList.list.length).delete(1), + new Delta().retain(this.richText.text.length).delete(1), ); // Sync Quill changes to our local state and to the server. @@ -111,10 +109,10 @@ export class QuillWrapper { [...Object.entries(quillAttrs)].map(quillAttrToFormatting), ); const [startPos, createdBunch, createdMarks] = - this.richList.insertWithFormat( + this.richText.insertWithFormat( deltaOp.index, formattingAttrs, - ...deltaOp.insert, + deltaOp.insert, ); if (createdBunch) { // Push meta op first to avoid missing BunchMeta deps. @@ -132,14 +130,14 @@ export class QuillWrapper { // Deletion else if (deltaOp.delete) { const toDelete = [ - ...this.richList.list.positions( + ...this.richText.text.positions( deltaOp.index, deltaOp.index + deltaOp.delete, ), ]; for (const pos of toDelete) { // OPT: group same-bunch deletions. - this.richList.list.delete(pos); + this.richText.text.delete(pos); wrapperOps.push({ type: 'delete', startPos: pos, @@ -153,7 +151,7 @@ export class QuillWrapper { deltaOp.attributes, )) { const [key, value] = quillAttrToFormatting([quillKey, quillValue]); - const [mark] = this.richList.format( + const [mark] = this.richText.format( deltaOp.index, deltaOp.index + deltaOp.retain, key, @@ -190,7 +188,7 @@ export class QuillWrapper { allMetas.push(op.meta); } } - this.richList.order.addMetas(allMetas); + this.richText.order.addMetas(allMetas); // Process the non-"meta" ops. let pendingDelta: DeltaStatic = new Delta(); @@ -206,12 +204,12 @@ export class QuillWrapper { // list.set so that it is not much slower to call it one-by-one // & post-batch the result for Quill. (What about getting the format? // I guess could use slice args.) - if (!this.richList.list.has(op.startPos)) { - this.richList.list.set(op.startPos, ...op.chars); - const startIndex = this.richList.list.indexOfPosition( + if (!this.richText.text.has(op.startPos)) { + this.richText.text.set(op.startPos, op.chars); + const startIndex = this.richText.text.indexOfPosition( op.startPos, ); - const format = this.richList.formatting.getFormat(op.startPos); + const format = this.richText.formatting.getFormat(op.startPos); pendingDelta = pendingDelta.compose( new Delta() .retain(startIndex) @@ -222,9 +220,9 @@ export class QuillWrapper { case 'delete': // OPT: group same-bunch deletions. for (const pos of expandPositions(op.startPos, op.count)) { - if (this.richList.list.has(pos)) { - const index = this.richList.list.indexOfPosition(pos); - this.richList.list.delete(pos); + if (this.richText.text.has(pos)) { + const index = this.richText.text.indexOfPosition(pos); + this.richText.text.delete(pos); pendingDelta = pendingDelta.compose( new Delta().retain(index).delete(1), ); @@ -233,10 +231,10 @@ export class QuillWrapper { break; case 'marks': for (const mark of op.marks) { - const changes = this.richList.formatting.addMark(mark); + const changes = this.richText.formatting.addMark(mark); for (const change of changes) { const {startIndex, endIndex} = sliceFromSpan( - this.richList.list, + this.richText.text, change.start, change.end, ); @@ -266,13 +264,13 @@ export class QuillWrapper { } /** - * Call to get an empty RichList, set initial state, then + * Call to get an empty RichText, set initial state, then * pass to constructor. */ - static newRichList(): RichList { - const richList = new RichList({expandRules}); - richList.load(this.makeInitialState()); - return richList; + static newRichText(): RichText { + const richText = new RichText({expandRules}); + richText.load(this.makeInitialState()); + return richText; } /** @@ -280,15 +278,15 @@ export class QuillWrapper { * "\n", to match Quill's initial state. */ private static makeInitialState() { - const richList = new RichList(); - const [pos] = richList.order.createPositions( + const richText = new RichText(); + const [pos] = richText.order.createPositions( MIN_POSITION, MAX_POSITION, 1, {bunchID: 'INIT'}, ); - richList.list.set(pos, '\n'); - return richList.save(); + richText.text.set(pos, '\n'); + return richText.save(); } } @@ -352,12 +350,12 @@ function getRelevantDeltaOperations(delta: DeltaStatic): { return relevantOps; } -function deltaFromSlices(slices: FormattedValues[]) { +function deltaFromSlices(slices: FormattedChars[]) { let delta = new Delta(); - for (const values of slices) { + for (const slice of slices) { delta = delta.insert( - values.values.join(''), - formattingToQuillAttr(values.format), + slice.chars, + formattingToQuillAttr(slice.format), ); } return delta; diff --git a/replicache-quill/package-lock.json b/replicache-quill/package-lock.json index 7563b8d..6534630 100644 --- a/replicache-quill/package-lock.json +++ b/replicache-quill/package-lock.json @@ -15,18 +15,21 @@ "devDependencies": { "@rocicorp/eslint-config": "^0.1.2", "@rocicorp/prettier-config": "^0.1.1", - "typescript": "4.7.4" + "typescript": "^5.4.5" }, "engines": { "node": ">=16.15.0", "npm": ">=7.0.0" } }, + "@list-positions/formatting": { + "extraneous": true + }, "client": { "version": "0.1.0", "dependencies": { - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.1", + "list-positions": "^1.0.0", "nanoid": "^4.0.0", "quill": "^1.3.6", "replicache": ">=14.0.3", @@ -38,13 +41,10 @@ "@types/quill": "^1.3.10", "concurrently": "^7.4.0", "prettier": "^2.2.1", - "typescript": "^4.6.4", + "typescript": "^5.4.5", "vite": "^3.0.7" } }, - "list-formatting": { - "extraneous": true - }, "list-positions": { "extraneous": true }, @@ -235,6 +235,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.1.tgz", + "integrity": "sha512-pk4msmrSm9Ogryt7/hlbh1kIGgQZAa3LXrgvKVw+BT7+kzew2DjovYJHFtrtbsHspycQ94WrbI/FMXn4u0YE/A==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2808,25 +2817,16 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" - }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -4436,9 +4436,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/spawn-command": { "version": "0.0.2-1", @@ -4691,16 +4691,16 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/undefsafe": { @@ -4949,10 +4949,10 @@ "server": { "version": "0.1.0", "dependencies": { + "@list-positions/formatting": "^1.0.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "nanoid": "^4.0.0", "replicache": ">=14.0.3", "replicache-transaction": "^0.3.3", @@ -4977,7 +4977,7 @@ "prettier": "^2.2.1", "shared": "^0.1.0", "ts-node": "^10.9.1", - "typescript": "4.7.4", + "typescript": "^5.4.5", "zod": ">=3.17.3" }, "engines": { @@ -4987,12 +4987,12 @@ "shared": { "version": "0.1.0", "devDependencies": { + "@list-positions/formatting": "^1.0.1", "@rocicorp/eslint-config": "^0.1.2", "@rocicorp/prettier-config": "^0.1.1", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "replicache": ">=14.0.3", - "typescript": "4.7.4" + "typescript": "^5.4.5" } } } diff --git a/replicache-quill/package.json b/replicache-quill/package.json index b34c62b..dd95cf1 100644 --- a/replicache-quill/package.json +++ b/replicache-quill/package.json @@ -4,7 +4,7 @@ "devDependencies": { "@rocicorp/eslint-config": "^0.1.2", "@rocicorp/prettier-config": "^0.1.1", - "typescript": "4.7.4" + "typescript": "^5.4.5" }, "scripts": { "format": "npm run format --ws", diff --git a/replicache-quill/server/package.json b/replicache-quill/server/package.json index 1b38075..34c586a 100644 --- a/replicache-quill/server/package.json +++ b/replicache-quill/server/package.json @@ -5,8 +5,8 @@ "dependencies": { "dotenv": "^16.0.1", "express": "^4.18.1", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.1", + "list-positions": "^1.0.0", "replicache": ">=14.0.3", "shared": "^0.1.0", "nanoid": "^4.0.0", @@ -31,7 +31,7 @@ "@types/node": "^16.11.50", "nodemon": "^2.0.19", "ts-node": "^10.9.1", - "typescript": "4.7.4", + "typescript": "^5.4.5", "zod": ">=3.17.3" }, "scripts": { diff --git a/replicache-quill/shared/package.json b/replicache-quill/shared/package.json index b0313d3..6c0ee40 100644 --- a/replicache-quill/shared/package.json +++ b/replicache-quill/shared/package.json @@ -3,11 +3,11 @@ "version": "0.1.0", "private": true, "devDependencies": { - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.1", + "list-positions": "^1.0.0", "@rocicorp/eslint-config": "^0.1.2", "@rocicorp/prettier-config": "^0.1.1", - "typescript": "4.7.4", + "typescript": "^5.4.5", "replicache": ">=14.0.3" }, "scripts": { diff --git a/replicache-quill/shared/src/mutators.ts b/replicache-quill/shared/src/mutators.ts index b2f4845..49d1df0 100644 --- a/replicache-quill/shared/src/mutators.ts +++ b/replicache-quill/shared/src/mutators.ts @@ -25,7 +25,7 @@ // on how Replicache syncs and resolves conflicts, but understanding that is not // required to get up and running. -import type {WriteTransaction} from 'replicache'; +import type {ReadonlyJSONValue, WriteTransaction} from 'replicache'; import { idOfMark, type AddMarks, @@ -90,7 +90,10 @@ export const mutators = { console.warn('addMarks: Skipping duplicate mark ID:', id); continue; } - await tx.set(`mark/${id}`, mark); + // ReadonlyJSONValue is supposed to express that the value is deep-readonly. + // Because of https://github.com/microsoft/TypeScript/issues/15300 , though, + // it doesn't work on JSON objects whose type is (or includes) an interface. + await tx.set(`mark/${id}`, mark as unknown as ReadonlyJSONValue); } }, }; diff --git a/replicache-quill/shared/src/rich_text.ts b/replicache-quill/shared/src/rich_text.ts index 7e8fb88..d5c482e 100644 --- a/replicache-quill/shared/src/rich_text.ts +++ b/replicache-quill/shared/src/rich_text.ts @@ -1,4 +1,4 @@ -import type {TimestampMark} from 'list-formatting'; +import type {TimestampMark} from '@list-positions/formatting'; import type {BunchMeta, Position} from 'list-positions'; import type {ReadTransaction} from 'replicache'; @@ -31,6 +31,9 @@ export type AddMarks = { marks: TimestampMark[]; }; -export async function allMarks(tx: ReadTransaction) { - return await tx.scan({prefix: 'mark/'}).values().toArray(); +export async function allMarks(tx: ReadTransaction): Promise { + // ReadonlyJSONValue is supposed to express that the value is deep-readonly. + // Because of https://github.com/microsoft/TypeScript/issues/15300 , though, + // it doesn't work on JSON objects whose type is (or includes) an interface. + return await tx.scan({prefix: 'mark/'}).values().toArray() as unknown as TimestampMark[]; } diff --git a/suggested-changes/package-lock.json b/suggested-changes/package-lock.json index 735875b..c0e3fc4 100644 --- a/suggested-changes/package-lock.json +++ b/suggested-changes/package-lock.json @@ -6,9 +6,9 @@ "": { "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-keymap": "^1.2.2", @@ -129,6 +129,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2482,25 +2491,16 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" - }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -3871,9 +3871,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/spdx-correct": { "version": "3.2.0", diff --git a/suggested-changes/package.json b/suggested-changes/package.json index ba0e365..fe99441 100644 --- a/suggested-changes/package.json +++ b/suggested-changes/package.json @@ -2,8 +2,8 @@ "license": "MIT", "dependencies": { "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.0", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-keymap": "^1.2.2", diff --git a/suggested-changes/src/common/block_text.ts b/suggested-changes/src/common/block_text.ts index d900db3..184cacb 100644 --- a/suggested-changes/src/common/block_text.ts +++ b/suggested-changes/src/common/block_text.ts @@ -1,4 +1,4 @@ -import { TimestampFormattingSavedState } from "list-formatting"; +import { TimestampFormattingSavedState } from "@list-positions/formatting"; import { ListSavedState, OrderSavedState } from "list-positions"; /** diff --git a/suggested-changes/src/common/messages.ts b/suggested-changes/src/common/messages.ts index c442310..8516059 100644 --- a/suggested-changes/src/common/messages.ts +++ b/suggested-changes/src/common/messages.ts @@ -1,4 +1,4 @@ -import { TimestampMark } from "list-formatting"; +import { TimestampMark } from "@list-positions/formatting"; import { BunchMeta, Position } from "list-positions"; import { BlockMarker, BlockTextSavedState } from "./block_text"; diff --git a/suggested-changes/src/server/rich_text_server.ts b/suggested-changes/src/server/rich_text_server.ts index b612f20..6b7cfed 100644 --- a/suggested-changes/src/server/rich_text_server.ts +++ b/suggested-changes/src/server/rich_text_server.ts @@ -1,4 +1,4 @@ -import { TimestampMark } from "list-formatting"; +import { TimestampMark } from "@list-positions/formatting"; import { List, Order } from "list-positions"; import { WebSocket, WebSocketServer } from "ws"; import { BlockMarker } from "../common/block_text"; @@ -12,7 +12,6 @@ export class RichTextServer { private readonly text: List; private readonly blockMarkers: List; // We don't need to inspect the formatting, so just store the marks directly. - // TODO: store in compareMarks order so we don't have to worry about it? private readonly marks: TimestampMark[]; private clients = new Set(); diff --git a/suggested-changes/src/site/index.html b/suggested-changes/src/site/index.html index 67a5fa5..6ff4261 100644 --- a/suggested-changes/src/site/index.html +++ b/suggested-changes/src/site/index.html @@ -40,7 +40,7 @@
Info and source code
diff --git a/suggested-changes/src/site/prosemirror_wrapper.ts b/suggested-changes/src/site/prosemirror_wrapper.ts index f891d91..9b8e375 100644 --- a/suggested-changes/src/site/prosemirror_wrapper.ts +++ b/suggested-changes/src/site/prosemirror_wrapper.ts @@ -4,7 +4,7 @@ import { TimestampMark, diffFormats, spanFromSlice, -} from "list-formatting"; +} from "@list-positions/formatting"; import { BunchMeta, List, diff --git a/suggested-changes/src/site/suggestion.ts b/suggested-changes/src/site/suggestion.ts index 3bb54f5..71186fd 100644 --- a/suggested-changes/src/site/suggestion.ts +++ b/suggested-changes/src/site/suggestion.ts @@ -1,4 +1,4 @@ -import { TimestampFormatting } from "list-formatting"; +import { TimestampFormatting } from "@list-positions/formatting"; import { List, MAX_POSITION, @@ -130,7 +130,6 @@ export class Suggestion { 1, { bunchID } ); - console.log(beforePos, newMeta); // TODO: use dedicated meta message instead of this empty set. this.messages.push({ diff --git a/triplit-quill/README.md b/triplit-quill/README.md index c49bd81..572c2f6 100644 --- a/triplit-quill/README.md +++ b/triplit-quill/README.md @@ -1,6 +1,6 @@ # Triplit-Quill -Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme), the [Triplit](https://www.triplit.dev/) fullstack database, and [Quill](https://quilljs.com/). +Basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme), the [Triplit](https://www.triplit.dev/) fullstack database, and [Quill](https://quilljs.com/). The editor state is stored in a Triplit database with three tables: diff --git a/triplit-quill/index.html b/triplit-quill/index.html index 502e584..468b7c0 100644 --- a/triplit-quill/index.html +++ b/triplit-quill/index.html @@ -10,7 +10,7 @@
Info and source code
diff --git a/triplit-quill/package-lock.json b/triplit-quill/package-lock.json index f4473bb..80a03c5 100644 --- a/triplit-quill/package-lock.json +++ b/triplit-quill/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "@triplit/client": "^0.3.1", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "quill": "^1.3.6" }, "devDependencies": { @@ -785,6 +785,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@mantine/form": { "version": "7.6.1", "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.6.1.tgz", @@ -3730,9 +3739,9 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/lie": { "version": "3.1.1", @@ -3759,21 +3768,12 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } - }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -5608,9 +5608,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/split-on-first": { "version": "1.1.0", diff --git a/triplit-quill/package.json b/triplit-quill/package.json index b371e72..1ae2470 100644 --- a/triplit-quill/package.json +++ b/triplit-quill/package.json @@ -17,8 +17,8 @@ }, "dependencies": { "@triplit/client": "^0.3.1", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.0", + "list-positions": "^1.0.0", "quill": "^1.3.6" } } diff --git a/triplit-quill/src/main.ts b/triplit-quill/src/main.ts index 335a96e..5be7127 100644 --- a/triplit-quill/src/main.ts +++ b/triplit-quill/src/main.ts @@ -1,5 +1,5 @@ import { ClientFetchResult, TriplitClient } from "@triplit/client"; -import { RichList } from "list-formatting"; +import { RichText } from "@list-positions/formatting"; import { MAX_POSITION, MIN_POSITION, @@ -180,13 +180,13 @@ async function sendLocalOps() { * "\n", to match Quill's initial state. */ function makeInitialState() { - const richList = new RichList(); + const richText = new RichText(); // Use the same bunchID & BunchMeta on all replicas. - const [pos] = richList.order.createPositions(MIN_POSITION, MAX_POSITION, 1, { + const [pos] = richText.order.createPositions(MIN_POSITION, MAX_POSITION, 1, { bunchID: "INIT", }); - richList.list.set(pos, "\n"); - return richList.save(); + richText.text.set(pos, "\n"); + return richText.save(); } function idOfPos(pos: Position): string { diff --git a/triplit-quill/src/quill_wrapper.ts b/triplit-quill/src/quill_wrapper.ts index 3462045..9dd1eb2 100644 --- a/triplit-quill/src/quill_wrapper.ts +++ b/triplit-quill/src/quill_wrapper.ts @@ -2,12 +2,12 @@ import Quill, { DeltaStatic, Delta as DeltaType } from "quill"; // Quill CSS. import { - FormattedValues, - RichList, - RichListSavedState, + FormattedChars, + RichText, + RichTextSavedState, TimestampMark, sliceFromSpan, -} from "list-formatting"; +} from "@list-positions/formatting"; import { BunchMeta, Position } from "list-positions"; import "quill/dist/quill.snow.css"; @@ -37,7 +37,7 @@ export class QuillWrapper { /** * Instead of editing this directly, use the applyOps method. */ - readonly richList: RichList; + readonly richText: RichText; private ourChange = false; @@ -52,12 +52,10 @@ export class QuillWrapper { readonly onLocalOps: (ops: WrapperOp[]) => void, /** * Must end in "\n" to match Quill, even if otherwise empty. - * - * Okay if marks are not in compareMarks order (weaker than RichListSavedState reqs). */ - initialState: RichListSavedState + initialState: RichTextSavedState ) { - this.richList = new RichList({ expandRules }); + this.richText = new RichText({ expandRules }); // Setup Quill. const editorContainer = document.getElementById("editor") as HTMLDivElement; @@ -76,27 +74,20 @@ export class QuillWrapper { formats: ["bold", "italic", "header", "list"], }); - // Load initial state into richList. - this.richList.order.load(initialState.order); - this.richList.list.load(initialState.list); - // initialState.marks is not a saved state; add directly. - for (const mark of initialState.formatting) { - this.richList.formatting.addMark(mark); - } + // Load initial state into richText. + this.richText.load(initialState); if ( - this.richList.list.length === 0 || - this.richList.list.getAt(this.richList.list.length - 1) !== "\n" + this.richText.text.length === 0 || + this.richText.text.getAt(this.richText.text.length - 1) !== "\n" ) { throw new Error('Bad initial state: must end in "\n" to match Quill'); } // Sync initial state to Quill. - this.editor.updateContents( - deltaFromSlices(this.richList.formattedValues()) - ); + this.editor.updateContents(deltaFromSlices(this.richText.formattedChars())); // Delete Quill's own initial "\n" - the initial state is supposed to end with one. this.editor.updateContents( - new Delta().retain(this.richList.list.length).delete(1) + new Delta().retain(this.richText.text.length).delete(1) ); // Sync Quill changes to our local state and to the server. @@ -114,10 +105,10 @@ export class QuillWrapper { [...Object.entries(quillAttrs)].map(quillAttrToFormatting) ); const [startPos, createdBunch, createdMarks] = - this.richList.insertWithFormat( + this.richText.insertWithFormat( deltaOp.index, formattingAttrs, - ...deltaOp.insert + deltaOp.insert ); if (createdBunch) { // Push meta op first to avoid missing BunchMeta deps. @@ -138,13 +129,13 @@ export class QuillWrapper { // Deletion else if (deltaOp.delete) { const toDelete = [ - ...this.richList.list.positions( + ...this.richText.text.positions( deltaOp.index, deltaOp.index + deltaOp.delete ), ]; for (const pos of toDelete) { - this.richList.list.delete(pos); + this.richText.text.delete(pos); wrapperOps.push({ type: "delete", pos, @@ -157,7 +148,7 @@ export class QuillWrapper { deltaOp.attributes )) { const [key, value] = quillAttrToFormatting([quillKey, quillValue]); - const [mark] = this.richList.format( + const [mark] = this.richText.format( deltaOp.index, deltaOp.index + deltaOp.retain, key, @@ -194,7 +185,7 @@ export class QuillWrapper { allMetas.push(op.meta); } } - this.richList.order.addMetas(allMetas); + this.richText.order.addMetas(allMetas); // Process the non-"meta" ops. let pendingDelta: DeltaStatic = new Delta(); @@ -210,12 +201,12 @@ export class QuillWrapper { // list.set so that it is not much slower to call it one-by-one // & post-batch the result for Quill. (What about getting the format? // I guess could use slice args.) - if (!this.richList.list.has(op.startPos)) { - this.richList.list.set(op.startPos, ...op.chars); - const startIndex = this.richList.list.indexOfPosition( + if (!this.richText.text.has(op.startPos)) { + this.richText.text.set(op.startPos, op.chars); + const startIndex = this.richText.text.indexOfPosition( op.startPos ); - const format = this.richList.formatting.getFormat(op.startPos); + const format = this.richText.formatting.getFormat(op.startPos); pendingDelta = pendingDelta.compose( new Delta() .retain(startIndex) @@ -224,19 +215,19 @@ export class QuillWrapper { } break; case "delete": - if (this.richList.list.has(op.pos)) { - const index = this.richList.list.indexOfPosition(op.pos); - this.richList.list.delete(op.pos); + if (this.richText.text.has(op.pos)) { + const index = this.richText.text.indexOfPosition(op.pos); + this.richText.text.delete(op.pos); pendingDelta = pendingDelta.compose( new Delta().retain(index).delete(1) ); } break; case "mark": - const changes = this.richList.formatting.addMark(op.mark); + const changes = this.richText.formatting.addMark(op.mark); for (const change of changes) { const { startIndex, endIndex } = sliceFromSpan( - this.richList.list, + this.richText.text, change.start, change.end ); @@ -325,13 +316,10 @@ function getRelevantDeltaOperations(delta: DeltaStatic): { return relevantOps; } -function deltaFromSlices(slices: FormattedValues[]) { +function deltaFromSlices(slices: FormattedChars[]) { let delta = new Delta(); - for (const values of slices) { - delta = delta.insert( - values.values.join(""), - formattingToQuillAttr(values.format) - ); + for (const slice of slices) { + delta = delta.insert(slice.chars, formattingToQuillAttr(slice.format)); } return delta; } diff --git a/websocket-prosemirror-blocks/README.md b/websocket-prosemirror-blocks/README.md index 0f139bb..b31003a 100644 --- a/websocket-prosemirror-blocks/README.md +++ b/websocket-prosemirror-blocks/README.md @@ -1,6 +1,6 @@ # WebSocket-Prosemirror-Blocks -A basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme), a WebSocket server, and [ProseMirror](https://prosemirror.net/). It supports only a simple block-based schema, stored in a "list-positions-native" way. +A basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme), a WebSocket server, and [ProseMirror](https://prosemirror.net/). It supports only a simple block-based schema, stored in a "list-positions-native" way. When a client makes a change, a description is sent to the server in JSON format. The server echoes that change to all other connected clients. The server also updates its own copy of the rich-text state; this is sent to new clients when they load the page. diff --git a/websocket-prosemirror-blocks/package-lock.json b/websocket-prosemirror-blocks/package-lock.json index c9d1cf6..2541e5a 100644 --- a/websocket-prosemirror-blocks/package-lock.json +++ b/websocket-prosemirror-blocks/package-lock.json @@ -6,9 +6,9 @@ "": { "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-keymap": "^1.2.2", @@ -129,6 +129,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2482,25 +2491,16 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" - }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -3871,9 +3871,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/spdx-correct": { "version": "3.2.0", diff --git a/websocket-prosemirror-blocks/package.json b/websocket-prosemirror-blocks/package.json index ba0e365..fe99441 100644 --- a/websocket-prosemirror-blocks/package.json +++ b/websocket-prosemirror-blocks/package.json @@ -2,8 +2,8 @@ "license": "MIT", "dependencies": { "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "@list-positions/formatting": "^1.0.0", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-keymap": "^1.2.2", diff --git a/websocket-prosemirror-blocks/src/common/block_text.ts b/websocket-prosemirror-blocks/src/common/block_text.ts index d900db3..184cacb 100644 --- a/websocket-prosemirror-blocks/src/common/block_text.ts +++ b/websocket-prosemirror-blocks/src/common/block_text.ts @@ -1,4 +1,4 @@ -import { TimestampFormattingSavedState } from "list-formatting"; +import { TimestampFormattingSavedState } from "@list-positions/formatting"; import { ListSavedState, OrderSavedState } from "list-positions"; /** diff --git a/websocket-prosemirror-blocks/src/common/messages.ts b/websocket-prosemirror-blocks/src/common/messages.ts index c442310..8516059 100644 --- a/websocket-prosemirror-blocks/src/common/messages.ts +++ b/websocket-prosemirror-blocks/src/common/messages.ts @@ -1,4 +1,4 @@ -import { TimestampMark } from "list-formatting"; +import { TimestampMark } from "@list-positions/formatting"; import { BunchMeta, Position } from "list-positions"; import { BlockMarker, BlockTextSavedState } from "./block_text"; diff --git a/websocket-prosemirror-blocks/src/server/rich_text_server.ts b/websocket-prosemirror-blocks/src/server/rich_text_server.ts index b612f20..6b7cfed 100644 --- a/websocket-prosemirror-blocks/src/server/rich_text_server.ts +++ b/websocket-prosemirror-blocks/src/server/rich_text_server.ts @@ -1,4 +1,4 @@ -import { TimestampMark } from "list-formatting"; +import { TimestampMark } from "@list-positions/formatting"; import { List, Order } from "list-positions"; import { WebSocket, WebSocketServer } from "ws"; import { BlockMarker } from "../common/block_text"; @@ -12,7 +12,6 @@ export class RichTextServer { private readonly text: List; private readonly blockMarkers: List; // We don't need to inspect the formatting, so just store the marks directly. - // TODO: store in compareMarks order so we don't have to worry about it? private readonly marks: TimestampMark[]; private clients = new Set(); diff --git a/websocket-prosemirror-blocks/src/site/index.html b/websocket-prosemirror-blocks/src/site/index.html index 547e276..c02539a 100644 --- a/websocket-prosemirror-blocks/src/site/index.html +++ b/websocket-prosemirror-blocks/src/site/index.html @@ -34,7 +34,7 @@
Info and source code
diff --git a/websocket-prosemirror-blocks/src/site/prosemirror_wrapper.ts b/websocket-prosemirror-blocks/src/site/prosemirror_wrapper.ts index ef3f4b1..d99850c 100644 --- a/websocket-prosemirror-blocks/src/site/prosemirror_wrapper.ts +++ b/websocket-prosemirror-blocks/src/site/prosemirror_wrapper.ts @@ -3,7 +3,7 @@ import { TimestampMark, diffFormats, spanFromSlice, -} from "list-formatting"; +} from "@list-positions/formatting"; import { BunchMeta, List, diff --git a/websocket-prosemirror-log/package-lock.json b/websocket-prosemirror-log/package-lock.json index 3a1397a..3aa2fae 100644 --- a/websocket-prosemirror-log/package-lock.json +++ b/websocket-prosemirror-log/package-lock.json @@ -6,9 +6,9 @@ "": { "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "express": "^4.18.2", - "list-formatting": "^0.8.1", - "list-positions": "^0.8.1", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-example-setup": "^1.2.2", @@ -133,6 +133,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2495,19 +2504,10 @@ "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, - "node_modules/list-formatting": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.8.1.tgz", - "integrity": "sha512-lyanWhCO8Xi6DIRaRgT4CmBdnApVcnysEAvWOZV+uNp+kQN5vWEvBp/tIXIBsPoYH9osuN7G+GScK+MPyVkJNw==", - "dependencies": { - "list-positions": "^0.8.1", - "maybe-random-string": "^1.0.0" - } - }, "node_modules/list-positions": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.8.1.tgz", - "integrity": "sha512-yxEuDYB4KE6fR+XnfmVltWjKRW4g+DwM423RiynaiVqs33GZeKSHjn8T00/NPKMILCTTL6/DPGywXXrcPxgRxQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", diff --git a/websocket-prosemirror-log/package.json b/websocket-prosemirror-log/package.json index 43f13bf..b2233ca 100644 --- a/websocket-prosemirror-log/package.json +++ b/websocket-prosemirror-log/package.json @@ -2,8 +2,8 @@ "license": "MIT", "dependencies": { "express": "^4.18.2", - "list-formatting": "^0.8.1", - "list-positions": "^0.8.1", + "@list-positions/formatting": "^1.0.0", + "list-positions": "^1.0.0", "maybe-random-string": "^1.0.0", "prosemirror-commands": "^1.5.2", "prosemirror-example-setup": "^1.2.2", diff --git a/websocket-prosemirror-log/src/common/messages.ts b/websocket-prosemirror-log/src/common/messages.ts index 7fadafa..fd10ff3 100644 --- a/websocket-prosemirror-log/src/common/messages.ts +++ b/websocket-prosemirror-log/src/common/messages.ts @@ -12,4 +12,3 @@ export type WelcomeMessage = { }; export type Message = MutationMessage | WelcomeMessage; - diff --git a/websocket-prosemirror-log/src/site/index.html b/websocket-prosemirror-log/src/site/index.html index b4a000c..69cf824 100644 --- a/websocket-prosemirror-log/src/site/index.html +++ b/websocket-prosemirror-log/src/site/index.html @@ -38,7 +38,7 @@ | Info and source code
diff --git a/websocket-quill/README.md b/websocket-quill/README.md index 5d3d188..0f0c98c 100644 --- a/websocket-quill/README.md +++ b/websocket-quill/README.md @@ -1,6 +1,6 @@ # WebSocket-Quill -A basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [list-formatting](https://github.com/mweidner037/list-formatting#readme), a WebSocket server, and [Quill](https://quilljs.com/). +A basic collaborative rich-text editor using [list-positions](https://github.com/mweidner037/list-positions#readme) and [@list-positions/formatting](https://github.com/mweidner037/list-positions-formatting#readme), a WebSocket server, and [Quill](https://quilljs.com/). When a client makes a change, a description is sent to the server in JSON format. The server echoes that change to all other connected clients. The server also updates its own copy of the rich-text state; this is sent to new clients when they load the page. diff --git a/websocket-quill/package-lock.json b/websocket-quill/package-lock.json index 87f1976..eff894e 100644 --- a/websocket-quill/package-lock.json +++ b/websocket-quill/package-lock.json @@ -6,9 +6,9 @@ "": { "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "quill": "^1.3.6", "quill-cursors": "^4.0.2", "ws": "^8.13.0" @@ -29,14 +29,14 @@ "rimraf": "^2.7.1", "source-map-loader": "^3.0.0", "style-loader": "^3.3.3", - "ts-loader": "^9.2.5", - "ts-node": "^10.1.0", - "typescript": "^4.3.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.4.5", "webpack": "^5.50.0", "webpack-cli": "^4.10.0" } }, - "../list-formatting": { + "../@list-positions/formatting": { "extraneous": true }, "../list-positions": { @@ -131,6 +131,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@list-positions/formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@list-positions/formatting/-/formatting-1.0.0.tgz", + "integrity": "sha512-0tXZpnPCXy0vNj1i87kn+sFUUj47PTVUswl5rfKYXVV7t3sOyfBTYIjVbQtV0sz84q012KtMmCXoySQowxo6nw==", + "dependencies": { + "list-positions": "^1.0.0", + "maybe-random-string": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2545,25 +2554,16 @@ } }, "node_modules/lex-sequence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-1.0.0.tgz", - "integrity": "sha512-59iqoNiaz0Qpf6eBQIUWczq+S20Ees7KhnfnOwsS5/4wLKcE8zG2/36PT1qD4Jwwef3HloyRnxQAHgYALLGlEw==" - }, - "node_modules/list-formatting": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-formatting/-/list-formatting-0.7.0.tgz", - "integrity": "sha512-3/LPBIhN+s1gU4HrRTMAqGmsDPvFxnuLATmQ3xwW29sYdjvNAzwVL4q7QSExH3gScYZseS31+eqRw90lFhF1lw==", - "dependencies": { - "list-positions": "^0.7.0", - "maybe-random-string": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lex-sequence/-/lex-sequence-2.0.0.tgz", + "integrity": "sha512-tKDpkkSZpkRJfqHgnPTTSAGohply3MdT8B31aYeMsrWMVFhN1k3+fGVa5GmLgxQMKsjPNN/3W/m9R3ewAGlNew==" }, "node_modules/list-positions": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-0.7.0.tgz", - "integrity": "sha512-6SNLSuZEK6p99hWipy8vMvXK2TWSbOCUvUAvtj/p6b6bEg/o6brlaM9WeKeEnX8mB5tzq3RhT20QRU6LmVwYdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/list-positions/-/list-positions-1.0.0.tgz", + "integrity": "sha512-AnwUGme9NuqaAe2VEa2UJafb3IirliB39WxxSmS3mGka0EP8MolSGxMr2BzAVtJBbD29kRgM8E00NOJlN3du6w==", "dependencies": { - "lex-sequence": "^1.0.0", + "lex-sequence": "^2.0.0", "maybe-random-string": "^1.0.0", "sparse-array-rled": "^1.0.0" } @@ -3922,9 +3922,9 @@ } }, "node_modules/sparse-array-rled": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.0.1.tgz", - "integrity": "sha512-zLMl/2QaaWA21ZhOSQ+tkN3FoZvJ3oYrnL1PJpv+XQBpTAJXHJol1/1kdZlUfXKeXxG9BOvhJXbQk9EhbAuREA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sparse-array-rled/-/sparse-array-rled-1.1.0.tgz", + "integrity": "sha512-rTKA5PX19/R8GnIeF/uZAjkZPXiPkuscOfFmDOyH0zQRKCDd5Gee2fFrrJ/Tw/G3Lxqhu9tN8pVdtVPPIvuE2g==" }, "node_modules/spdx-correct": { "version": "3.2.0", @@ -4459,16 +4459,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { diff --git a/websocket-quill/package.json b/websocket-quill/package.json index 86d5c5f..95d9fca 100644 --- a/websocket-quill/package.json +++ b/websocket-quill/package.json @@ -1,9 +1,9 @@ { "license": "MIT", "dependencies": { + "@list-positions/formatting": "^1.0.0", "express": "^4.18.2", - "list-formatting": "^0.7.0", - "list-positions": "^0.7.0", + "list-positions": "^1.0.0", "quill": "^1.3.6", "quill-cursors": "^4.0.2", "ws": "^8.13.0" @@ -24,9 +24,9 @@ "rimraf": "^2.7.1", "source-map-loader": "^3.0.0", "style-loader": "^3.3.3", - "ts-loader": "^9.2.5", - "ts-node": "^10.1.0", - "typescript": "^4.3.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.4.5", "webpack": "^5.50.0", "webpack-cli": "^4.10.0" }, diff --git a/websocket-quill/src/common/messages.ts b/websocket-quill/src/common/messages.ts index cf20ba3..e425007 100644 --- a/websocket-quill/src/common/messages.ts +++ b/websocket-quill/src/common/messages.ts @@ -1,9 +1,12 @@ -import { TimestampMark } from "list-formatting"; +import { + TimestampFormattingSavedState, + TimestampMark, +} from "@list-positions/formatting"; import { BunchMeta, - ListSavedState, OrderSavedState, Position, + TextSavedState, } from "list-positions"; export type SetMessage = { @@ -26,10 +29,8 @@ export type MarkMessage = { export type WelcomeMessage = { type: "welcome"; order: OrderSavedState; - list: ListSavedState; - // Note: these are in receipt order, *not* timestamp order. - // So you can't use them as a TimestampFormattingSavedState. - marks: TimestampMark[]; + text: TextSavedState; + formatting: TimestampFormattingSavedState; }; export type Message = SetMessage | DeleteMessage | MarkMessage | WelcomeMessage; diff --git a/websocket-quill/src/server/rich_text_server.ts b/websocket-quill/src/server/rich_text_server.ts index d4ffd12..c693bd9 100644 --- a/websocket-quill/src/server/rich_text_server.ts +++ b/websocket-quill/src/server/rich_text_server.ts @@ -1,5 +1,5 @@ -import { TimestampMark } from "list-formatting"; -import { List } from "list-positions"; +import { TimestampMark } from "@list-positions/formatting"; +import { Text } from "list-positions"; import { WebSocket, WebSocketServer } from "ws"; import { Message } from "../common/messages"; @@ -8,20 +8,18 @@ const heartbeatInterval = 30000; export class RichTextServer { // To easily save and send the state to new clients, store the // text in a List. - private readonly list: List; + private readonly text: Text; // We don't need to inspect the formatting, so just store the marks directly. - // Note: these are in receipt order, *not* timestamp order. - // So you can't use them as a TimestampFormattingSavedState. private readonly marks: TimestampMark[]; private clients = new Set(); constructor(readonly wss: WebSocketServer) { - this.list = new List(); + this.text = new Text(); this.marks = []; // Initial state: a single "\n", to match Quill's initial state. - this.list.insertAt(0, "\n"); + this.text.insertAt(0, "\n"); this.wss.on("connection", (ws) => { if (ws.readyState === WebSocket.OPEN) { @@ -57,9 +55,9 @@ export class RichTextServer { // Send the current state. this.sendMessage(ws, { type: "welcome", - order: this.list.order.save(), - list: this.list.save(), - marks: this.marks, + order: this.text.order.save(), + text: this.text.save(), + formatting: this.marks, }); this.clients.add(ws); @@ -84,9 +82,9 @@ export class RichTextServer { switch (msg.type) { case "set": if (msg.meta) { - this.list.order.addMetas([msg.meta]); + this.text.order.addMetas([msg.meta]); } - this.list.set(msg.startPos, ...msg.chars); + this.text.set(msg.startPos, msg.chars); this.echo(ws, data); // Because a Position is only ever set once (when it's created) and // the server does no validation, the origin's optimistically-updated @@ -95,7 +93,7 @@ export class RichTextServer { // telling it how to repair its optimistically-updated state. break; case "delete": - this.list.delete(msg.pos); + this.text.delete(msg.pos); this.echo(ws, data); // Because deletes are permanant and the server does no validation, // the origin's optimistically-updated state is already correct. diff --git a/websocket-quill/src/site/index.html b/websocket-quill/src/site/index.html index 05349c4..c67e2aa 100644 --- a/websocket-quill/src/site/index.html +++ b/websocket-quill/src/site/index.html @@ -10,7 +10,7 @@
Info and source code
diff --git a/websocket-quill/src/site/main.ts b/websocket-quill/src/site/main.ts index 8e9d5f9..cabfe4d 100644 --- a/websocket-quill/src/site/main.ts +++ b/websocket-quill/src/site/main.ts @@ -19,4 +19,4 @@ ws.addEventListener("message", welcomeListener); // For this basic demo, we don't allow disconnection tests or // attempt to reconnect the WebSocket ever. // That would require buffering updates and/or logic to -// "merge" in the Welcome state received after reconnecting. \ No newline at end of file +// "merge" in the Welcome state received after reconnecting. diff --git a/websocket-quill/src/site/quill_wrapper.ts b/websocket-quill/src/site/quill_wrapper.ts index ef5dfb5..0f246ec 100644 --- a/websocket-quill/src/site/quill_wrapper.ts +++ b/websocket-quill/src/site/quill_wrapper.ts @@ -1,7 +1,11 @@ import Quill, { DeltaStatic, Delta as DeltaType } from "quill"; // Quill CSS. -import { FormattedValues, RichList, sliceFromSpan } from "list-formatting"; +import { + FormattedChars, + RichText, + sliceFromSpan, +} from "@list-positions/formatting"; import "quill/dist/quill.snow.css"; import { Message, WelcomeMessage } from "../common/messages"; @@ -9,10 +13,10 @@ const Delta: typeof DeltaType = Quill.import("delta"); export class QuillWrapper { readonly editor: Quill; - readonly richList: RichList; + readonly richText: RichText; constructor(readonly ws: WebSocket, welcome: WelcomeMessage) { - this.richList = new RichList({ expandRules }); + this.richText = new RichText({ expandRules }); // Setup Quill. const editorContainer = document.getElementById("editor") as HTMLDivElement; @@ -31,19 +35,16 @@ export class QuillWrapper { formats: ["bold", "italic", "header", "list"], }); - // Load initial state into richList. - this.richList.order.load(welcome.order); - this.richList.list.load(welcome.list); - // welcome.marks is not a saved state; add directly. - for (const mark of welcome.marks) this.richList.formatting.addMark(mark); + // Load initial state into richText. + this.richText.order.load(welcome.order); + this.richText.text.load(welcome.text); + this.richText.formatting.load(welcome.formatting); // Sync initial state to Quill. - this.editor.updateContents( - deltaFromSlices(this.richList.formattedValues()) - ); + this.editor.updateContents(deltaFromSlices(this.richText.formattedChars())); // Delete Quill's own initial "\n" - the server's state already contains one. this.editor.updateContents( - new Delta().retain(this.richList.list.length).delete(1) + new Delta().retain(this.richText.text.length).delete(1) ); // Sync Quill changes to our local state and to the server. @@ -61,10 +62,10 @@ export class QuillWrapper { [...Object.entries(quillAttrs)].map(quillAttrToFormatting) ); const [startPos, createdBunch, createdMarks] = - this.richList.insertWithFormat( + this.richText.insertWithFormat( op.index, formattingAttrs, - ...op.insert + op.insert ); this.send({ type: "set", @@ -86,10 +87,10 @@ export class QuillWrapper { // Deletion else if (op.delete) { const toDelete = [ - ...this.richList.list.positions(op.index, op.index + op.delete), + ...this.richText.text.positions(op.index, op.index + op.delete), ]; for (const pos of toDelete) { - this.richList.list.delete(pos); + this.richText.text.delete(pos); this.send({ type: "delete", pos, @@ -100,7 +101,7 @@ export class QuillWrapper { else if (op.attributes && op.retain) { for (const [quillKey, quillValue] of Object.entries(op.attributes)) { const [key, value] = quillAttrToFormatting([quillKey, quillValue]); - const [mark] = this.richList.format( + const [mark] = this.richText.format( op.index, op.index + op.retain, key, @@ -123,14 +124,14 @@ export class QuillWrapper { switch (msg.type) { case "set": if (msg.meta) { - this.richList.order.addMetas([msg.meta]); + this.richText.order.addMetas([msg.meta]); } // Sets are always nontrivial. // Because the server enforces causal ordering, bunched values // are always still contiguous and have a single format. - this.richList.list.set(msg.startPos, ...msg.chars); - const startIndex = this.richList.list.indexOfPosition(msg.startPos); - const format = this.richList.formatting.getFormat(msg.startPos); + this.richText.text.set(msg.startPos, msg.chars); + const startIndex = this.richText.text.indexOfPosition(msg.startPos); + const format = this.richText.formatting.getFormat(msg.startPos); this.editor.updateContents( new Delta() .retain(startIndex) @@ -138,17 +139,17 @@ export class QuillWrapper { ); break; case "delete": - if (this.richList.list.has(msg.pos)) { - const index = this.richList.list.indexOfPosition(msg.pos); - this.richList.list.delete(msg.pos); + if (this.richText.text.has(msg.pos)) { + const index = this.richText.text.indexOfPosition(msg.pos); + this.richText.text.delete(msg.pos); this.editor.updateContents(new Delta().retain(index).delete(1)); } break; case "mark": - const changes = this.richList.formatting.addMark(msg.mark); + const changes = this.richText.formatting.addMark(msg.mark); for (const change of changes) { const { startIndex, endIndex } = sliceFromSpan( - this.richList.list, + this.richText.text, change.start, change.end ); @@ -238,13 +239,10 @@ function getRelevantDeltaOperations(delta: DeltaStatic): { return relevantOps; } -function deltaFromSlices(slices: FormattedValues[]) { +function deltaFromSlices(slices: FormattedChars[]) { let delta = new Delta(); - for (const values of slices) { - delta = delta.insert( - values.values.join(""), - formattingToQuillAttr(values.format) - ); + for (const slice of slices) { + delta = delta.insert(slice.chars, formattingToQuillAttr(slice.format)); } return delta; }