Skip to content

Commit

Permalink
Update to 1.0 (#9)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mweidner037 authored Apr 27, 2024
1 parent e40879f commit 05a5d77
Show file tree
Hide file tree
Showing 52 changed files with 403 additions and 425 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).) <!-- TODO: add more as they are created -->

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.
Expand Down
2 changes: 1 addition & 1 deletion electricsql-quill/README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion electricsql-quill/db/migrations/01-create_docs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 21 additions & 21 deletions electricsql-quill/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions electricsql-quill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion electricsql-quill/src/quill/ElectricQuill.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
92 changes: 41 additions & 51 deletions electricsql-quill/src/quill/quill_wrapper.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -50,7 +50,7 @@ export class QuillWrapper {
/**
* Instead of editing this directly, use the applyOps method.
*/
readonly richList: RichList<string>;
readonly richText: RichText;

private ourChange = false;

Expand All @@ -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<string>,
initialState: RichTextSavedState,
order?: Order
) {
this.richList = new RichList({ expandRules, order });
this.richText = new RichText({ expandRules, order });

// Setup Quill.
this.editor = new Quill(container, {
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand All @@ -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)
);
Expand All @@ -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
);
Expand Down Expand Up @@ -268,34 +266,29 @@ export class QuillWrapper {
*
* Note: Order is not cleared, just appended.
*/
load(savedState: RichListSavedState<string>): 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;
Expand All @@ -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),
};
}

Expand All @@ -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,
Expand All @@ -336,15 +329,15 @@ export class QuillWrapper {
* "\n", to match Quill's initial state.
*/
static makeInitialState() {
const richList = new RichList<string>();
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();
}
}

Expand Down Expand Up @@ -408,13 +401,10 @@ function getRelevantDeltaOperations(delta: DeltaStatic): {
return relevantOps;
}

function deltaFromSlices(slices: FormattedValues<string>[]) {
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;
}
Expand Down
4 changes: 2 additions & 2 deletions replicache-quill/README.md
Original file line number Diff line number Diff line change
@@ -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/<bunchID>` 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/<mark ID>` 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/<mark ID>` 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:

Expand Down
2 changes: 1 addition & 1 deletion replicache-quill/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<body>
<div>
<a
href="https://github.com/mweidner037/list-demos/tree/master/replicache-quill#readme"
href="https://github.com/mweidner037/list-positions-demos/tree/master/replicache-quill#readme"
>Info and source code</a
>
<hr />
Expand Down
Loading

0 comments on commit 05a5d77

Please sign in to comment.