Compare commits

...

10 Commits

Author SHA1 Message Date
Anselm
55d1e7b03a More progress 2024-07-31 10:37:10 +01:00
Anselm
0f37a2ba69 Merge branch 'main' into rich-text 2024-07-29 11:23:52 +01:00
Anselm
00019c7578 More plaintext progress 2024-06-06 11:47:17 +01:00
Anselm
cdb0ac2b9f Rich text progress 2024-06-04 17:54:28 +01:00
Anselm
a1d84c433c Merge branch 'main' into rich-text 2024-06-04 13:32:18 +01:00
Anselm
40eb81135c Progress on text 2024-06-03 16:48:18 +01:00
Anselm
65fad3d84c Merge branch 'main' into rich-text 2024-06-01 19:36:27 +02:00
Anselm
00cad168c9 More implementation 2024-05-06 11:59:14 +01:00
Anselm
ce5475f127 Merge branch 'jazz-schema' into rich-text 2024-05-03 16:36:15 +01:00
Anselm
3095caaa6d Implement RawCoPlainText 2024-05-03 14:27:35 +01:00
32 changed files with 2516 additions and 27 deletions

View File

@@ -0,0 +1,13 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {},
}

24
examples/richtext/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,528 @@
# jazz-example-chat
## 0.0.57
### Patch Changes
- Updated dependencies
- cojson@0.7.10
- jazz-react@0.7.10
- jazz-tools@0.7.10
## 0.0.56
### Patch Changes
- Updated dependencies
- cojson@0.7.9
- jazz-react@0.7.9
- jazz-tools@0.7.9
## 0.0.55
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.8
- jazz-react@0.7.8
## 0.0.54
### Patch Changes
- Updated dependencies [9fdc91c]
- jazz-react@0.7.7
## 0.0.53
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.6
- jazz-react@0.7.6
## 0.0.52
### Patch Changes
- Updated dependencies
- jazz-react@0.7.5
## 0.0.51
### Patch Changes
- Updated dependencies
- jazz-react@0.7.4
## 0.0.50
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.3
- jazz-react@0.7.3
## 0.0.49
### Patch Changes
- Updated dependencies
- jazz-react@0.7.2
## 0.0.48
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.1
- jazz-react@0.7.1
## 0.0.47
### Patch Changes
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [96c494f]
- Updated dependencies [59c18c3]
- Updated dependencies [19f52b7]
- Updated dependencies [8636319]
- Updated dependencies [1a35307]
- Updated dependencies [d8fe2b1]
- Updated dependencies [19004b4]
- Updated dependencies [a78f168]
- Updated dependencies [1200aae]
- Updated dependencies [60d5ca2]
- Updated dependencies [52675c9]
- Updated dependencies [129e2c1]
- Updated dependencies [6d49e9b]
- Updated dependencies [1cfa279]
- Updated dependencies [704af7d]
- Updated dependencies [e97f730]
- Updated dependencies [1a35307]
- Updated dependencies [460478f]
- Updated dependencies [6b0418f]
- Updated dependencies [e299c3e]
- Updated dependencies [ed5643a]
- Updated dependencies [bde684f]
- Updated dependencies [bf0f8ec]
- Updated dependencies [c4151fc]
- Updated dependencies [63374cc]
- Updated dependencies [8636319]
- Updated dependencies [01ac646]
- Updated dependencies [a5e68a4]
- Updated dependencies [8636319]
- Updated dependencies [952982e]
- Updated dependencies [1a35307]
- Updated dependencies [5fa277c]
- Updated dependencies [60d5ca2]
- Updated dependencies [21771c4]
- Updated dependencies [77c2b56]
- Updated dependencies [63374cc]
- Updated dependencies [d2e03ff]
- Updated dependencies [354bdcd]
- Updated dependencies [ece35b3]
- Updated dependencies [60d5ca2]
- Updated dependencies [69ac514]
- Updated dependencies [f8a5c46]
- Updated dependencies [f0f6f1b]
- Updated dependencies [e5eed5b]
- Updated dependencies [1a44f87]
- Updated dependencies [627d895]
- Updated dependencies [1200aae]
- Updated dependencies [63374cc]
- Updated dependencies [ece35b3]
- Updated dependencies [38d4410]
- Updated dependencies [85d2b62]
- Updated dependencies [fd86c11]
- Updated dependencies [52675c9]
- jazz-tools@0.7.0
- cojson@0.7.0
- jazz-react@0.7.0
- hash-slash@0.2.0
## 0.0.47-alpha.42
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.42
- cojson@0.7.0-alpha.42
- jazz-react@0.7.0-alpha.42
## 0.0.47-alpha.41
### Patch Changes
- jazz-tools@0.7.0-alpha.41
- jazz-react@0.7.0-alpha.41
## 0.0.47-alpha.40
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.40
## 0.0.47-alpha.39
### Patch Changes
- Updated dependencies
- cojson@0.7.0-alpha.39
- jazz-react@0.7.0-alpha.39
- jazz-tools@0.7.0-alpha.39
## 0.0.47-alpha.38
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.38
- jazz-react@0.7.0-alpha.38
- cojson@0.7.0-alpha.38
## 0.0.47-alpha.37
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.37
- cojson@0.7.0-alpha.37
- jazz-tools@0.7.0-alpha.37
## 0.0.47-alpha.36
### Patch Changes
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [1a35307]
- Updated dependencies [6b0418f]
- Updated dependencies [1a35307]
- cojson@0.7.0-alpha.36
- jazz-tools@0.7.0-alpha.36
- jazz-react@0.7.0-alpha.36
## 0.0.47-alpha.35
### Patch Changes
- Updated dependencies
- Updated dependencies
- cojson@0.7.0-alpha.35
- jazz-tools@0.7.0-alpha.35
- jazz-react@0.7.0-alpha.35
## 0.0.47-alpha.34
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.34
- jazz-react@0.7.0-alpha.34
## 0.0.47-alpha.33
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.33
## 0.0.47-alpha.32
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- hash-slash@0.2.0-alpha.3
- jazz-tools@0.7.0-alpha.32
- jazz-react@0.7.0-alpha.32
## 0.0.47-alpha.31
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.31
- jazz-react@0.7.0-alpha.31
## 0.0.47-alpha.30
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.30
- jazz-react@0.7.0-alpha.30
## 0.0.47-alpha.29
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.29
- cojson@0.7.0-alpha.29
- jazz-react@0.7.0-alpha.29
## 0.0.47-alpha.28
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.28
- cojson@0.7.0-alpha.28
- jazz-react@0.7.0-alpha.28
## 0.0.47-alpha.27
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.27
- cojson@0.7.0-alpha.27
- jazz-react@0.7.0-alpha.27
## 0.0.47-alpha.26
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.26
- jazz-react@0.7.0-alpha.26
## 0.0.47-alpha.25
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.25
- jazz-react@0.7.0-alpha.25
## 0.0.47-alpha.24
### Patch Changes
- Updated dependencies
- Updated dependencies
- Updated dependencies
- jazz-tools@0.7.0-alpha.24
- cojson@0.7.0-alpha.24
- jazz-react@0.7.0-alpha.24
## 0.0.47-alpha.23
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.23
- jazz-react@0.7.0-alpha.23
## 0.0.47-alpha.22
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.22
- jazz-react@0.7.0-alpha.22
## 0.0.47-alpha.21
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.21
- jazz-tools@0.7.0-alpha.21
## 0.0.47-alpha.20
### Patch Changes
- Updated dependencies
- Updated dependencies
- jazz-react@0.7.0-alpha.20
- jazz-tools@0.7.0-alpha.20
## 0.0.47-alpha.19
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.19
- jazz-react@0.7.0-alpha.19
## 0.0.47-alpha.18
### Patch Changes
- jazz-react@0.7.0-alpha.18
## 0.0.47-alpha.17
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.17
- jazz-react@0.7.0-alpha.17
## 0.0.47-alpha.16
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.16
- jazz-react@0.7.0-alpha.16
## 0.0.47-alpha.15
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.15
- jazz-react@0.7.0-alpha.15
## 0.0.47-alpha.14
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.14
- jazz-react@0.7.0-alpha.14
## 0.0.47-alpha.13
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.13
- jazz-react@0.7.0-alpha.13
## 0.0.47-alpha.12
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.12
- jazz-tools@0.7.0-alpha.12
## 0.0.47-alpha.11
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.11
- jazz-tools@0.7.0-alpha.11
- cojson@0.7.0-alpha.11
## 0.0.47-alpha.10
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.10
- jazz-tools@0.7.0-alpha.10
- cojson@0.7.0-alpha.10
## 0.0.47-alpha.9
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.9
- jazz-tools@0.7.0-alpha.9
## 0.0.47-alpha.8
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.8
- jazz-tools@0.7.0-alpha.8
## 0.0.47-alpha.7
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.7
- jazz-tools@0.7.0-alpha.7
- cojson@0.7.0-alpha.7
## 0.0.47-alpha.6
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.6
- jazz-tools@0.7.0-alpha.6
## 0.0.47-alpha.5
### Patch Changes
- Updated dependencies
- jazz-react@0.7.0-alpha.5
- jazz-tools@0.7.0-alpha.5
- cojson@0.7.0-alpha.5
## 0.0.47-alpha.4
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.4
- jazz-react@0.7.0-alpha.4
## 0.0.47-alpha.3
### Patch Changes
- Updated dependencies
- jazz-tools@0.7.0-alpha.3
- jazz-react@0.7.0-alpha.3
## 0.0.47-alpha.2
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.2
- jazz-react@0.7.0-alpha.2
- jazz-tools@0.7.0-alpha.2
## 0.0.47-alpha.1
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.1
- jazz-react@0.7.0-alpha.1
- jazz-tools@0.7.0-alpha.1
- cojson@0.7.0-alpha.1
## 0.0.47-alpha.0
### Patch Changes
- Updated dependencies
- hash-slash@0.2.0-alpha.0
- jazz-react@0.7.0-alpha.0
- jazz-tools@0.7.0-alpha.0
- cojson@0.7.0-alpha.0
## 0.0.46
### Patch Changes
- Updated dependencies
- jazz-react@0.5.0
- jazz-react-auth-local@0.4.16

View File

@@ -0,0 +1,4 @@
FROM caddy:2.7.3-alpine
LABEL org.opencontainers.image.source="https://github.com/gardencmp/jazz"
COPY ./dist /usr/share/caddy/

View File

@@ -0,0 +1,42 @@
# Jazz Chat Example
Live version: https://example-chat.jazz.tools
## Installing & running the example locally
(this requires `pnpm` to be installed, see [https://pnpm.io/installation](https://pnpm.io/installation))
Start by checking out `jazz`
```bash
git clone https://github.com/gardencmp/jazz.git
cd jazz/examples/chat
pnpm pack --pack-destination /tmp
mkdir -p ~/jazz-examples/chat # or any other directory
tar -xf /tmp/jazz-example-chat-* --strip-components 1 -C ~/jazz-examples/chat
cd ~/jazz-examples/chat
```
This ensures that you have the example app without git history and independent of the Jazz multi-package monorepo.
Install dependencies:
```bash
pnpm install
```
Start the dev server:
```bash
pnpm dev
```
## Questions / problems / feedback
If you have feedback, let us know on [Discord](https://discord.gg/utDMjHYg42) or open an issue or PR to fix something that seems wrong.
## Configuration: sync server
By default, the example app uses [Jazz Global Mesh](https://jazz.tools/mesh) (`wss://sync.jazz.tools`) - so cross-device use, invites and collaboration should just work.
You can also run a local sync server by running `npx cojson-simple-sync` and adding the query param `?sync=ws://localhost:4200` to the URL of the example app (for example: `http://localhost:5173/?sync=ws://localhost:4200`), or by setting the `sync` parameter of the `<Jazz.Provider>` provider component in [./src/2_main.tsx](./src/2_main.tsx).

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/jazz-logo.png" />
<link rel="stylesheet" href="/src/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Chat Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
job "chat$BRANCH_SUFFIX" {
region = "global"
datacenters = ["*"]
group "static" {
count = 4
network {
port "http" {
to = 80
}
}
constraint {
attribute = "${node.class}"
operator = "="
value = "mesh"
}
spread {
attribute = "${node.datacenter}"
weight = 100
}
constraint {
distinct_hosts = true
}
task "server" {
driver = "docker"
config {
image = "$DOCKER_TAG"
ports = ["http"]
auth = {
username = "$DOCKER_USER"
password = "$DOCKER_PASSWORD"
}
}
service {
tags = ["public"]
name = "chat$BRANCH_SUFFIX"
port = "http"
provider = "consul"
}
resources {
cpu = 50 # MHz
memory = 50 # MB
}
}
}
}
# deploy bump 4

View File

@@ -0,0 +1,61 @@
{
"name": "jazz-example-richtext",
"private": true,
"version": "0.0.57",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "echo 'chat example is codegolfed'",
"preview": "vite preview"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{js,jsx,mdx,json}": "prettier --write"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@types/qrcode": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:*",
"hash-slash": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"prosemirror-example-setup": "^1.2.2",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.21.1",
"prosemirror-schema-basic": "^1.2.2",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.9.0",
"prosemirror-view": "^1.33.7",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uniqolor": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^5.0.10"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,313 @@
import { Group, ID, CoRichText, Marks, TreeNode, TreeLeaf } from "jazz-tools";
import { createJazzReactContext, DemoAuth } from "jazz-react";
import { createRoot } from "react-dom/client";
import { useIframeHashRouter } from "hash-slash";
import { useEffect, useState } from "react";
export class Document extends CoRichText {}
const Jazz = createJazzReactContext({
auth: DemoAuth({ appName: "Jazz Richtext Doc" }),
peer: `wss://mesh.jazz.tools/?key=you@example.com`,
});
export const { useAccount, useCoState } = Jazz;
function App() {
const { me, logOut } = useAccount();
const createDocument = () => {
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
const Doc = Document.createFromPlainTextAndMark("", Marks.Paragraph, {tag: "paragraph"}, { owner: me });
setTimeout(() => {
location.hash = "/doc/" + Doc.id;
}, 1000);
};
return (
<div className="flex flex-col items-center w-screen h-screen p-2 dark:bg-black dark:text-white">
<div className="rounded mb-5 px-2 py-1 text-sm self-end">
{me.profile?.name} · <button onClick={logOut}>Log Out</button>
</div>
{useIframeHashRouter().route({
"/": () => createDocument() as never,
"/doc/:id": (id) => (
<DocumentComponent docID={id as ID<Document>} />
),
})}
</div>
);
}
createRoot(document.getElementById("root")!).render(
<Jazz.Provider>
<App />
</Jazz.Provider>
);
import {
EditorState,
Transaction as ProsemirrorTransaction,
TextSelection,
} from "prosemirror-state";
import {
Node as ProsemirrorNode,
Mark as ProsemirrorMark,
} from "prosemirror-model";
import { ReplaceStep, AddMarkStep } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
import { schema } from "prosemirror-schema-basic";
import { exampleSetup } from "prosemirror-example-setup";
import "prosemirror-example-setup/style/style.css";
import "prosemirror-menu/style/menu.css";
import "prosemirror-view/style/prosemirror.css";
function DocumentComponent({ docID }: { docID: ID<Document> }) {
const { me } = useAccount();
const [mount, setMount] = useState<HTMLElement | null>(null);
console.log("rerendering");
useEffect(() => {
if (!mount) return;
console.log("Creating EditorView");
const setupPlugins = exampleSetup({ schema, history: false });
console.log("setupPlugins", setupPlugins, schema);
const editorView = new EditorView(mount, {
state: EditorState.create({
doc: schema.node("doc", undefined, [
schema.node("paragraph", undefined, undefined),
]),
schema: schema,
plugins: setupPlugins,
}),
dispatchTransaction(tr) {
const expectedNewState = editorView.state.apply(tr);
if (lastDoc) {
applyTxToPlainText(lastDoc, tr);
}
console.log(
"Setting view state to normal new state",
expectedNewState
);
editorView.updateState(expectedNewState);
},
});
let lastDoc: Document | undefined;
const unsub = Document.subscribe(
docID,
me,
{ text: true, marks: [[]] },
(doc) => {
lastDoc = doc;
console.log("Applying doc update");
console.log(
"marks",
doc.toString(),
doc.resolveAndDiffuseAndFocusMarks()
);
console.log("tree", doc.toTree(["strong", "em"]));
console.log(richTextToProsemirrorDoc(doc));
const focusedBefore = editorView.hasFocus();
editorView.updateState(
EditorState.create({
doc: richTextToProsemirrorDoc(doc),
plugins: editorView.state.plugins,
selection: editorView.state.selection,
schema: editorView.state.schema,
storedMarks: editorView.state.storedMarks,
})
);
if (focusedBefore) {
editorView.focus();
}
}
);
return () => {
console.log("Destroying");
editorView.destroy();
unsub();
};
}, [mount, docID, !!me]);
return (
<div>
<h1>Document</h1>
<div ref={setMount} className="border min-w-96 p-5 min-h-96" />
</div>
);
}
function richTextToProsemirrorDoc(
text: CoRichText
): ProsemirrorNode | undefined {
const asString = text.toString();
return schema.node("doc", undefined, [
schema.node(
"paragraph",
{ start: 0, end: asString.length },
asString.length === 0
? undefined
: text.toTree(["strong", "em"]).children.map((child) => {
if (
child.type === "leaf" ||
child.tag === "strong" ||
child.tag === "em"
) {
return collectInlineMarks(asString, child, []);
} else {
throw new Error("Unsupported tag " + child.tag);
}
})
),
]);
}
function collectInlineMarks(
fullString: string,
node: TreeNode | TreeLeaf,
currentMarks: ProsemirrorMark[]
) {
if (node.type === "leaf") {
return schema.text(
fullString.slice(node.start, node.end),
currentMarks
);
} else {
if (node.tag === "strong") {
return collectInlineMarks(
fullString,
node.children[0],
currentMarks.concat(schema.mark("strong"))
);
} else if (node.tag === "em") {
return collectInlineMarks(
fullString,
node.children[0],
currentMarks.concat(schema.mark("em"))
);
} else {
throw new Error("Unsupported tag " + node.tag);
}
}
}
function applyTxToPlainText(text: CoRichText, tr: ProsemirrorTransaction) {
console.log("transaction", tr);
for (const step of tr.steps) {
if (step instanceof ReplaceStep) {
const resolvedStart = tr.before.resolve(step.from);
const resolvedEnd = tr.before.resolve(step.to);
const selectionToStart = TextSelection.between(
tr.before.resolve(0),
resolvedStart
);
const start = selectionToStart
.content()
.content.textBetween(
0,
selectionToStart.content().content.size
).length;
const selectionToEnd = TextSelection.between(
tr.before.resolve(0),
resolvedEnd
);
const end = selectionToEnd
.content()
.content.textBetween(
0,
selectionToEnd.content().content.size
).length;
console.log(
"step",
step,
resolvedStart,
resolvedEnd,
selectionToStart,
start,
end
);
if (start === end) {
if (step.slice.content.firstChild?.text) {
text.insertAfter(start, step.slice.content.firstChild.text);
} else {
// this is a split operation
const splitNodeType =
step.slice.content.firstChild?.type.name;
if (splitNodeType === "paragraph") {
const matchingMarks =
text.marks?.filter(
(m): m is Exclude<typeof m, null> =>
!!m &&
m.tag === "paragraph" &&
(m.startAfter && text.idxAfter(m.startAfter) || 0) <
start &&
(m.endBefore && text.idxBefore(m.endBefore) || Infinity) >
start
) || [];
console.log("split before", start, matchingMarks);
let lastSeenEnd = start;
for (const matchingMark of matchingMarks) {
const originalEnd = text.idxAfter(
matchingMark.endAfter
)!; // TODO: non-tight case
if (originalEnd > lastSeenEnd) {
lastSeenEnd = originalEnd;
}
matchingMark.endBefore = text.posBefore(start + 1)!;
matchingMark.endAfter = text.posAfter(start)!;
}
console.log("split after", matchingMarks, lastSeenEnd);
text.insertMark(start, lastSeenEnd, Marks.Paragraph, {
tag: "paragraph",
});
} else {
console.warn(
"Unknown node type to split",
splitNodeType
);
}
}
} else {
text.deleteRange({ from: start, to: end });
}
} else if (step instanceof AddMarkStep) {
console.log("step", step);
if (step.mark.type.name === "strong") {
text.insertMark(step.from, step.to - 1, Marks.Strong, {
tag: "strong",
});
} else if (step.mark.type.name === "em") {
text.insertMark(step.from, step.to - 1, Marks.Em, {
tag: "em",
});
} else {
console.warn("Unsupported mark type", step.mark);
}
} else {
console.warn("Unsupported step type", step);
}
}
}

View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
margin: 0;
padding: 0;
}
}

1
examples/richtext/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
build: {
minify: false
}
})

View File

@@ -6,6 +6,7 @@ import { RawCoList } from "./coValues/coList.js";
import { CoValueCore } from "./coValueCore.js";
import { RawGroup } from "./coValues/group.js";
import { RawAccount, Profile } from "./index.js";
import { RawCoPlainText } from "./coValues/coPlainText.js";
export type CoID<T extends RawCoValue> = RawCoID & {
readonly __type: T;
@@ -66,3 +67,11 @@ export function expectStream(content: RawCoValue): RawCoStream {
return content as RawCoStream;
}
export function expectPlainText(content: RawCoValue): RawCoPlainText {
if (content.type !== "coplaintext") {
throw new Error("Expected plaintext");
}
return content as RawCoPlainText;
}

View File

@@ -7,9 +7,9 @@ import { AgentID, SessionID, TransactionID } from "../ids.js";
import { AccountID } from "./account.js";
import { RawGroup } from "./group.js";
type OpID = TransactionID & { changeIdx: number };
export type OpID = TransactionID & { changeIdx: number };
type InsertionOpPayload<T extends JsonValue> =
export type InsertionOpPayload<T extends JsonValue> =
| {
op: "pre";
value: T;
@@ -21,7 +21,7 @@ type InsertionOpPayload<T extends JsonValue> =
after: OpID | "start";
};
type DeletionOpPayload = {
export type DeletionOpPayload = {
op: "del";
insertion: OpID;
};
@@ -49,7 +49,7 @@ export class RawCoListView<
/** @category 6. Meta */
id: CoID<this>;
/** @category 6. Meta */
type = "colist" as const;
type: "colist" | "coplaintext" = "colist" as const;
/** @category 6. Meta */
core: CoValueCore;
/** @internal */
@@ -446,13 +446,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
/**
@@ -499,13 +493,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
/** Deletes the item at index `at`.
@@ -532,13 +520,7 @@ export class RawCoList<
privacy,
);
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;
this.beforeEnd = listAfter.beforeEnd;
this.insertions = listAfter.insertions;
this.deletionsByInsertion = listAfter.deletionsByInsertion;
this._cachedEntries = undefined;
this.rebuildFromCore();
}
replace(
@@ -566,6 +548,11 @@ export class RawCoList<
],
privacy,
);
this.rebuildFromCore();
}
/** @internal */
rebuildFromCore() {
const listAfter = new RawCoList(this.core) as this;
this.afterStart = listAfter.afterStart;

View File

@@ -0,0 +1,129 @@
import { CoValueCore } from "../coValueCore.js";
import { JsonObject } from "../jsonValue.js";
import { DeletionOpPayload, InsertionOpPayload, OpID, RawCoList } from "./coList.js";
export type StringifiedOpID = string & { __stringifiedOpID: true };
export function stringifyOpID(opID: OpID): StringifiedOpID {
return `${opID.sessionID}:${opID.txIndex}:${opID.changeIdx}` as StringifiedOpID;
}
type PlaintextIdxMapping = {
opIDbeforeIdx: OpID[];
opIDafterIdx: OpID[];
idxAfterOpID: { [opID: StringifiedOpID]: number };
idxBeforeOpID: { [opID: StringifiedOpID]: number };
};
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
export class RawCoPlainText<
Meta extends JsonObject | null = JsonObject | null,
> extends RawCoList<string, Meta> {
/** @category 6. Meta */
type = "coplaintext" as const;
_cachedMapping: WeakMap<
NonNullable<typeof this._cachedEntries>,
PlaintextIdxMapping
>;
constructor(core: CoValueCore) {
super(core);
this._cachedMapping = new WeakMap();
}
get mapping() {
const entries = this.entries();
let mapping = this._cachedMapping.get(entries);
if (mapping) {
return mapping;
}
mapping = {
opIDbeforeIdx: [],
opIDafterIdx: [],
idxAfterOpID: {},
idxBeforeOpID: {},
};
let idxBefore = 0;
for (const entry of entries) {
const idxAfter = idxBefore + entry.value.length;
mapping.opIDafterIdx[idxBefore] = entry.opID;
mapping.opIDbeforeIdx[idxAfter] = entry.opID;
mapping.idxAfterOpID[stringifyOpID(entry.opID)] = idxAfter;
mapping.idxBeforeOpID[stringifyOpID(entry.opID)] = idxBefore;
idxBefore = idxAfter;
}
this._cachedMapping.set(entries, mapping);
return mapping;
}
toString() {
return this.entries()
.map((entry) => entry.value)
.join("");
}
insertAfter(
idx: number,
text: string,
privacy: "private" | "trusting" = "private"
) {
const ops: InsertionOpPayload<string>[] = [];
let prevOpId: OpID | "start" | undefined = this.mapping.opIDbeforeIdx[idx];
if (!prevOpId) {
if (idx === 0) {
prevOpId = "start"
} else {
throw new Error("Invalid idx");
}
}
const nextTxId = this.core.nextTransactionID();
let changeIdx = 0;
for (const grapheme of segmenter.segment(text)) {
ops.push({
op: "app",
value: grapheme.segment,
after: prevOpId,
});
prevOpId = {
sessionID: nextTxId.sessionID,
txIndex: nextTxId.txIndex,
changeIdx,
};
changeIdx++;
}
this.core.makeTransaction(ops, privacy);
this.rebuildFromCore();
}
deleteRange({from, to}: {from: number, to: number}, privacy: "private" | "trusting" = "private") {
const ops: DeletionOpPayload[] = [];
for (let idx = from; idx < to;) {
const insertion = this.mapping.opIDafterIdx[idx];
if (!insertion) {
throw new Error("Invalid idx to delete " + (idx));
}
ops.push({
op: "del",
insertion,
});
console.log("deleting idx", idx)
let nextIdx = idx + 1;
while (!this.mapping.opIDbeforeIdx[nextIdx] && nextIdx < to) {
nextIdx++;
}
idx = nextIdx;
}
this.core.makeTransaction(ops, privacy);
this.rebuildFromCore();
}
}

View File

@@ -8,6 +8,7 @@ import { AgentID, isAgentID } from "../ids.js";
import { RawAccount, AccountID, ControlledAccountOrAgent } from "./account.js";
import { Role } from "../permissions.js";
import { base58 } from "@scure/base";
import { RawCoPlainText } from "./coPlainText.js";
export const EVERYONE = "everyone" as const;
export type Everyone = "everyone";
@@ -307,6 +308,36 @@ export class RawGroup<
return list;
}
/**
* Creates a new `CoList` within this group, with the specified specialized
* `CoList` type `L` and optional static metadata.
*
* @category 3. Value creation
*/
createPlainText<T extends RawCoPlainText>(
init?: string,
meta?: T["headerMeta"],
initPrivacy: "trusting" | "private" = "private"
): T {
const text = this.core.node
.createCoValue({
type: "coplaintext",
ruleset: {
type: "ownedByGroup",
group: this.id,
},
meta: meta || null,
...this.core.crypto.createdNowUnique(),
})
.getCurrentContent() as T;
if (init) {
text.insertAfter(0, init, initPrivacy);
}
return text;
}
/** @category 3. Value creation */
createStream<C extends RawCoStream>(meta?: C["headerMeta"]): C {
return this.core.node

View File

@@ -5,6 +5,7 @@ import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoStream } from "./coValues/coStream.js";
import { RawBinaryCoStream } from "./coValues/coStream.js";
import { RawCoPlainText } from "./coValues/coPlainText.js";
export function coreToCoValue(
core: CoValueCore,
@@ -38,6 +39,8 @@ export function coreToCoValue(
} else {
return new RawCoStream(core);
}
} else if (core.header.type === "coplaintext") {
return new RawCoPlainText(core);
} else {
throw new Error(`Unknown coValue type ${core.header.type}`);
}

View File

@@ -9,6 +9,7 @@ import { LocalNode } from "./localNode.js";
import type { RawCoValue } from "./coValue.js";
import { RawCoMap } from "./coValues/coMap.js";
import { RawCoList } from "./coValues/coList.js";
import { RawCoPlainText, stringifyOpID } from "./coValues/coPlainText.js";
import { RawCoStream, RawBinaryCoStream } from "./coValues/coStream.js";
import {
secretSeedLength,
@@ -94,6 +95,7 @@ export {
Everyone,
RawCoMap,
RawCoList,
RawCoPlainText,
RawCoStream,
RawBinaryCoStream,
RawCoValue,
@@ -122,6 +124,7 @@ export {
PureJSCrypto,
SyncMessage,
isRawCoID,
stringifyOpID,
LSMStorage,
DisconnectedError,
PingTimeoutError,
@@ -153,4 +156,5 @@ export namespace CojsonInternalTypes {
export type SealerSecret = import("./crypto/crypto.js").SealerSecret;
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
export type OpID = import("./coValues/coList.js").OpID;
}

View File

@@ -0,0 +1,73 @@
import { expect, test } from "vitest";
import { expectPlainText } from "../coValue.js";
import { WasmCrypto } from "../index.js";
import { LocalNode } from "../localNode.js";
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
const Crypto = await WasmCrypto.create();
test("Empty CoPlainText works", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
expect(content.toString()).toEqual("");
});
test("Can insert into empty CoPlainText", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
content.insertAfter(0, "a", "trusting");
expect(content.toString()).toEqual("a");
});
test("Can insert and delete in CoPlainText", () => {
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
const coValue = node.createCoValue({
type: "coplaintext",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...Crypto.createdNowUnique(),
});
const content = expectPlainText(coValue.getCurrentContent());
expect(content.type).toEqual("coplaintext");
content.insertAfter(0, "hello", "trusting");
expect(content.toString()).toEqual("hello");
content.insertAfter(5, " world", "trusting");
expect(content.toString()).toEqual("hello world");
console.log("first delete")
content.deleteRange({from: 3, to: 8}, "trusting");
expect(content.toString()).toEqual("helrld");
content.insertAfter(2, "😍", "trusting");
expect(content.toString()).toEqual("he😍lrld")
console.log("second delete")
content.deleteRange({from: 2, to: 4}, "trusting");
expect(content.toString()).toEqual("helrld");
})

View File

@@ -0,0 +1,211 @@
import type { CojsonInternalTypes, RawCoPlainText } from "cojson";
import { RawAccount, stringifyOpID } from "cojson";
import type {
AccountCtx,
CoValue,
CoValueClass,
ID,
UnavailableError,
} from "../internal.js";
import { Account, Group, inspect, loadCoValue, loadCoValueEf, subscribeToCoValue, subscribeToCoValueEf, subscribeToExistingCoValue } from "../internal.js";
import type { Effect, Stream } from "effect";
export type TextPos = CojsonInternalTypes.OpID;
export class CoPlainText
extends String
implements CoValue
{
declare id: ID<this>;
declare _type: "CoPlainText";
declare _raw: RawCoPlainText;
get _owner(): Account | Group {
return this._raw.group instanceof RawAccount
? Account.fromRaw(this._raw.group)
: Group.fromRaw(this._raw.group);
}
get _loadedAs() {
return Account.fromNode(this._raw.core.node);
}
constructor(options: { fromRaw: RawCoPlainText } | { text: string, owner: Account | Group }) {
super();
let raw;
if ("fromRaw" in options) {
raw = options.fromRaw;
} else {
raw = options.owner._raw.createPlainText(options.text);
}
Object.defineProperties(this, {
id: { value: raw.id, enumerable: false },
_type: { value: "CoPlainText", enumerable: false },
_raw: { value: raw, enumerable: false },
});
}
static create<T extends CoPlainText>(this: CoValueClass<T>, text: string, options: { owner: Account | Group }) {
return new this({ text, owner: options.owner });
}
toString() {
return this._raw.toString();
}
valueOf() {
return this._raw.toString();
}
toJSON(): string {
return this._raw.toString();
}
[inspect]() {
return this.toJSON();
}
insertAfter(idx: number, text: string) {
this._raw.insertAfter(idx, text);
}
deleteRange(range: {from: number, to: number}) {
this._raw.deleteRange(range);
}
posBefore(idx: number): TextPos | undefined {
return this._raw.mapping.opIDbeforeIdx[idx];
}
posAfter(idx: number): TextPos | undefined {
return this._raw.mapping.opIDafterIdx[idx];
}
idxBefore(pos: TextPos): number | undefined {
return this._raw.mapping.idxBeforeOpID[stringifyOpID(pos)];
}
idxAfter(pos: TextPos): number | undefined {
return this._raw.mapping.idxAfterOpID[stringifyOpID(pos)];
}
static fromRaw<V extends CoPlainText>(
this: CoValueClass<V> & typeof CoPlainText,
raw: RawCoPlainText
) {
return new this({ fromRaw: raw });
}
/**
* Load a `CoPlainText` with a given ID, as a given account.
*
* `depth` specifies which (if any) fields that reference other CoValues to load as well before resolving.
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
*
* You can pass `[]` or `{}` for shallowly loading only this CoPlainText, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
*
* @example
* ```ts
* const person = await Person.load(
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
* me,
* { pet: {} }
* );
* ```
*
* @category Subscription & Loading
*/
static load<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
as: Account,
): Promise<T | undefined> {
return loadCoValue(this, id, as, []);
}
/**
* Effectful version of `CoMap.load()`.
*
* Needs to be run inside an `AccountCtx` context.
*
* @category Subscription & Loading
*/
static loadEf<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
): Effect.Effect<T, UnavailableError, AccountCtx> {
return loadCoValueEf(this, id, []);
}
/**
* Load and subscribe to a `CoMap` with a given ID, as a given account.
*
* Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
*
* `depth` specifies which (if any) fields that reference other CoValues to load as well before calling `listener` for the first time.
* The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
*
* You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
*
* Returns an unsubscribe function that you should call when you no longer need updates.
*
* Also see the `useCoState` hook to reactively subscribe to a CoValue in a React component.
*
* @example
* ```ts
* const unsub = Person.subscribe(
* "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
* me,
* { pet: {} },
* (person) => console.log(person)
* );
* ```
*
* @category Subscription & Loading
*/
static subscribe<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
as: Account,
listener: (value: T) => void,
): () => void {
return subscribeToCoValue(this, id, as, [], listener);
}
/**
* Effectful version of `CoMap.subscribe()` that returns a stream of updates.
*
* Needs to be run inside an `AccountCtx` context.
*
* @category Subscription & Loading
*/
static subscribeEf<T extends CoPlainText>(
this: CoValueClass<T>,
id: ID<T>,
): Stream.Stream<T, UnavailableError, AccountCtx> {
return subscribeToCoValueEf(this, id, []);
}
/**
* Given an already loaded `CoMap`, subscribe to updates to the `CoMap` and ensure that the specified fields are loaded to the specified depth.
*
* Works like `CoMap.subscribe()`, but you don't need to pass the ID or the account to load as again.
*
* Returns an unsubscribe function that you should call when you no longer need updates.
*
* @category Subscription & Loading
**/
subscribe<T extends CoPlainText>(
this: T,
listener: (value: T) => void,
): () => void {
return subscribeToExistingCoValue(this, [], listener);
}
}

View File

@@ -0,0 +1,396 @@
import type { Account, CoMapInit, Group, TextPos } from "../internal.js";
import { CoList, CoMap, CoPlainText, co } from "../internal.js";
export class Mark extends CoMap {
startAfter = co.json<TextPos | null>();
startBefore = co.json<TextPos>();
endAfter = co.json<TextPos>();
endBefore = co.json<TextPos | null>();
tag = co.string;
}
export type ResolvedMark<R extends Mark = Mark> = {
startAfter: number;
startBefore: number;
endAfter: number;
endBefore: number;
sourceMark: R;
};
export type ResolvedAndDiffusedMark<R extends Mark = Mark> = {
start: number;
end: number;
side: "uncertainStart" | "certainMiddle" | "uncertainEnd";
sourceMark: R;
};
export type FocusBias = "far" | "close" | "closestWhitespace";
export type ResolvedAndFocusedMark<R extends Mark = Mark> = {
start: number;
end: number;
sourceMark: R;
};
export class CoRichText extends CoMap {
text = co.ref(CoPlainText);
marks = co.ref(CoList.Of(co.ref(Mark)));
static createFromPlainText(
text: string,
options: { owner: Account | Group },
) {
return this.create(
{
text: CoPlainText.create(text, { owner: options.owner }),
marks: CoList.Of(co.ref(Mark)).create([], {
owner: options.owner,
}),
},
{ owner: options.owner },
);
}
static createFromPlainTextAndMark<MarkClass extends {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): Mark;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
create(init: any, options: { owner: Account | Group }): Mark;
}>(
text: string,
WrapIn: MarkClass,
extraArgs: Omit<
CoMapInit<InstanceType<MarkClass>>,
"startAfter" | "startBefore" | "endAfter" | "endBefore"
>,
options: { owner: Account | Group },
) {
const richtext = this.createFromPlainText(text, options);
richtext.insertMark(0, text.length, WrapIn, extraArgs);
return richtext;
}
insertAfter(idx: number, text: string) {
if (!this.text)
throw new Error(
"Cannot insert into a CoRichText without loaded text",
);
this.text.insertAfter(idx, text);
}
deleteRange(range: { from: number; to: number }) {
if (!this.text)
throw new Error(
"Cannot delete from a CoRichText without loaded text",
);
this.text.deleteRange(range);
}
posBefore(idx: number): TextPos | undefined {
if (!this.text)
throw new Error(
"Cannot get posBefore in a CoRichText without loaded text",
);
return this.text.posBefore(idx);
}
posAfter(idx: number): TextPos | undefined {
if (!this.text)
throw new Error(
"Cannot get posAfter in a CoRichText without loaded text",
);
return this.text.posAfter(idx);
}
idxBefore(pos: TextPos): number | undefined {
if (!this.text)
throw new Error(
"Cannot get idxBefore in a CoRichText without loaded text",
);
return this.text.idxBefore(pos);
}
idxAfter(pos: TextPos): number | undefined {
if (!this.text)
throw new Error(
"Cannot get idxAfter in a CoRichText without loaded text",
);
return this.text.idxAfter(pos);
}
insertMark<
MarkClass extends {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): Mark;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
create(init: any, options: { owner: Account | Group }): Mark;
},
>(
start: number,
end: number,
RangeClass: MarkClass,
extraArgs: Omit<
CoMapInit<InstanceType<MarkClass>>,
"startAfter" | "startBefore" | "endAfter" | "endBefore"
>,
options?: { markOwner?: Account | Group },
) {
if (!this.marks) {
throw new Error("Cannot insert a range without loaded ranges");
}
const range = RangeClass.create(
{
...extraArgs,
startAfter: this.posBefore(start),
startBefore: this.posAfter(start),
endAfter: this.posBefore(end),
endBefore: this.posAfter(end),
},
{ owner: options?.markOwner || this._owner },
);
this.marks.push(range);
}
resolveMarks(): ResolvedMark[] {
if (!this.text || !this.marks) {
throw new Error(
"Cannot resolve ranges without loaded text and ranges",
);
}
const ranges = this.marks.flatMap((mark) => {
if (!mark) return [];
const startBefore = this.idxAfter(mark.startBefore);
const endAfter = this.idxAfter(mark.endAfter);
if (
startBefore === undefined ||
endAfter === undefined
) {
return [];
}
const startAfter = mark.startAfter ? this.idxAfter(mark.startAfter) : startBefore - 1;
const endBefore = mark.endBefore ? this.idxAfter(mark.endBefore) : endAfter + 1;
if (
startAfter === undefined ||
endBefore === undefined
) {
return [];
}
return [
{
sourceMark: mark,
startAfter,
startBefore,
endAfter,
endBefore,
tag: mark.tag,
from: mark,
},
];
});
return ranges;
}
resolveAndDiffuseMarks(): ResolvedAndDiffusedMark[] {
return this.resolveMarks().flatMap((range) => [
...(range.startAfter < range.startBefore - 1
? [
{
start: range.startAfter,
end: range.startBefore - 1,
side: "uncertainStart" as const,
sourceMark: range.sourceMark,
},
]
: []),
{
start: range.startBefore - 1,
end: range.endAfter + 1,
side: "certainMiddle" as const,
sourceMark: range.sourceMark,
},
...(range.endAfter + 1 < range.endBefore
? [
{
start: range.endAfter + 1,
end: range.endBefore,
side: "uncertainEnd" as const,
sourceMark: range.sourceMark,
},
]
: []),
]);
}
resolveAndDiffuseAndFocusMarks(): ResolvedAndFocusedMark[] {
// for now we only keep the certainMiddle ranges
return this.resolveAndDiffuseMarks().filter(
(range) => range.side === "certainMiddle",
);
}
toTree(tagPrecedence: string[]): TreeNode {
const ranges = this.resolveAndDiffuseAndFocusMarks();
// convert a bunch of (potentially overlapping) ranges into a tree
// - make sure we include all text in leaves, even if it's not covered by a range
// - we split overlapping ranges in a way where the higher precedence (tag earlier in tagPrecedence)
// stays intact and the lower precende tag is split into two ranges, one inside and one outside the higher precedence range
const text = this.text?.toString() || "";
let currentNodes: (TreeLeaf | TreeNode)[] = [
{
type: "leaf",
start: 0,
end: text.length,
},
];
const rangesSortedLowToHighPrecedence = ranges.sort((a, b) => {
const aPrecedence = tagPrecedence.indexOf(a.sourceMark.tag);
const bPrecedence = tagPrecedence.indexOf(b.sourceMark.tag);
return bPrecedence - aPrecedence;
});
// for each range, split the current nodes where necessary (no matter if leaf or already a node), wrapping the resulting "inside" parts in a node with the range's tag
for (const range of rangesSortedLowToHighPrecedence) {
// console.log("currentNodes", currentNodes);
const newNodes = currentNodes.flatMap((node) => {
const [before, inOrAfter] = splitNode(node, range.start);
const [inside, after] = inOrAfter
? splitNode(inOrAfter, range.end)
: [undefined, undefined];
// console.log("split", range.start, range.end, {
// before,
// inside,
// after,
// });
// TODO: also split children
return [
...(before ? [before] : []),
...(inside
? [
{
type: "node" as const,
tag: range.sourceMark.tag,
start: inside.start,
end: inside.end,
children: [inside],
},
]
: []),
...(after ? [after] : []),
];
});
currentNodes = newNodes;
}
return {
type: "node",
tag: "root",
start: 0,
end: text.length,
children: currentNodes,
};
}
toString() {
if (!this.text) return "";
return this.text.toString();
}
}
export type TreeLeaf = {
type: "leaf";
start: number;
end: number;
};
export type TreeNode = {
type: "node";
tag: string;
start: number;
end: number;
range?: ResolvedAndFocusedMark;
children: (TreeNode | TreeLeaf)[];
};
function splitNode(
node: TreeNode | TreeLeaf,
at: number,
): [TreeNode | TreeLeaf | undefined, TreeNode | TreeLeaf | undefined] {
if (node.type === "leaf") {
return [
at > node.start
? {
type: "leaf",
start: node.start,
end: Math.min(at, node.end),
}
: undefined,
at < node.end
? {
type: "leaf",
start: Math.max(at, node.start),
end: node.end,
}
: undefined,
];
} else {
const children = node.children;
return [
at > node.start
? {
type: "node",
tag: node.tag,
start: node.start,
end: Math.min(at, node.end),
children: children
.map((child) => splitNode(child, at)[0])
.filter(
(c): c is Exclude<typeof c, undefined> => !!c,
),
}
: undefined,
at < node.end
? {
type: "node",
tag: node.tag,
start: Math.max(at, node.start),
end: node.end,
children: children
.map((child) => splitNode(child, at)[1])
.filter(
(c): c is Exclude<typeof c, undefined> => !!c,
),
}
: undefined,
];
}
}
export const Marks = {
Heading: class Heading extends Mark {
tag = co.literal("heading");
level = co.number;
},
Paragraph: class Paragraph extends Mark {
tag = co.literal("paragraph");
},
Link: class Link extends Mark {
tag = co.literal("link");
url = co.string;
},
Strong: class Strong extends Mark {
tag = co.literal("strong");
},
Em: class Italic extends Mark {
tag = co.literal("em");
},
};

View File

@@ -81,7 +81,10 @@ export function fulfillsDepth(depth: any, value: CoValue): boolean {
).optional,
);
}
} else if (value._type === "BinaryCoStream") {
} else if (
value._type === "BinaryCoStream" ||
value._type === "CoPlainText"
) {
return true;
} else {
console.error(value);
@@ -226,4 +229,10 @@ export type DeeplyLoaded<
},
]
? V
: never;
: [V] extends [
{
_type: "CoPlainText";
},
]
? V
: never;

View File

@@ -19,6 +19,8 @@ export { Encoders, co } from "./internal.js";
export { CoMap, type CoMapInit } from "./internal.js";
export { CoList } from "./internal.js";
export { CoPlainText, TextPos } from "./internal.js";
export { CoRichText, Mark, Marks, TreeNode, TreeLeaf } from "./internal.js";
export { CoStream, BinaryCoStream } from "./internal.js";
export { Group, Profile } from "./internal.js";
export { Account, isControlledAccount } from "./internal.js";

View File

@@ -5,6 +5,8 @@ export * from "./coValues/interfaces.js";
export * from "./coValues/coMap.js";
export * from "./coValues/account.js";
export * from "./coValues/coList.js";
export * from "./coValues/coPlainText.js";
export * from "./coValues/coRichText.js";
export * from "./coValues/coStream.js";
export * from "./coValues/group.js";

View File

@@ -0,0 +1,33 @@
import { expect, describe, test } from "vitest";
import { Account, CoPlainText, WasmCrypto } from "../index.js";
const Crypto = await WasmCrypto.create();
describe("Simple CoPlainText operations", async () => {
const me = await Account.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto
});
const text = CoPlainText.create("hello world", { owner: me });
test("Construction", () => {
expect(text + "").toEqual("hello world");
});
describe("Mutation", () => {
test("insertion", () => {
const text = CoPlainText.create("hello world", { owner: me });
text.insertAfter(5, " cruel");
expect(text + "").toEqual("hello cruel world");
});
test("deletion", () => {
const text = CoPlainText.create("hello world", { owner: me });
text.deleteRange({from: 3, to: 8});
expect(text + "").toEqual("helrld");
});
});
});

View File

@@ -0,0 +1,59 @@
import { expect, describe, test } from "vitest";
import { Account, CoRichText, Marks, WasmCrypto } from "../index.js";
const Crypto = await WasmCrypto.create();
describe("Simple CoRichText operations", async () => {
const me = await Account.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto
});
const text = CoRichText.createFromPlainText("hello world", { owner: me });
test("Construction", () => {
expect(text + "").toEqual("hello world");
});
describe("Mutation", () => {
test("insertion", () => {
const text = CoRichText.createFromPlainText("hello world", {
owner: me,
});
text.insertAfter(5, " cruel");
expect(text + "").toEqual("hello cruel world");
});
test("deletion", () => {
const text = CoRichText.createFromPlainText("hello world", {
owner: me,
});
text.deleteRange({from: 3, to: 8});
expect(text + "").toEqual("helrld");
});
test("inserting ranges", () => {
const text = CoRichText.createFromPlainText("hello world", {
owner: me,
});
text.insertMark(6, 9, Marks.Strong, { tag: "strong" });
console.log(text.text?._raw.entries());
console.log(text.text?._raw.mapping);
expect(text.resolveMarks()).toEqual([
{
startAfter: 6,
startBefore: 7,
endAfter: 9,
endBefore: 10,
tag: "bold",
from: text.marks![0],
},
]);
});
});
});

271
pnpm-lock.yaml generated
View File

@@ -330,6 +330,130 @@ importers:
specifier: ^4.4.5
version: 4.5.1(@types/node@20.10.5)
examples/richtext:
dependencies:
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.45)(react@18.2.0)
'@radix-ui/react-toast':
specifier: ^1.1.4
version: 1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/qrcode':
specifier: ^1.5.1
version: 1.5.5
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
clsx:
specifier: ^2.0.0
version: 2.0.0
cojson:
specifier: workspace:*
version: link:../../packages/cojson
hash-slash:
specifier: workspace:*
version: link:../../packages/hash-slash
jazz-react:
specifier: workspace:*
version: link:../../packages/jazz-react
jazz-tools:
specifier: workspace:*
version: link:../../packages/jazz-tools
lucide-react:
specifier: ^0.274.0
version: 0.274.0(react@18.2.0)
prosemirror-example-setup:
specifier: ^1.2.2
version: 1.2.2
prosemirror-menu:
specifier: ^1.2.4
version: 1.2.4
prosemirror-model:
specifier: ^1.21.1
version: 1.21.1
prosemirror-schema-basic:
specifier: ^1.2.2
version: 1.2.2
prosemirror-state:
specifier: ^1.4.3
version: 1.4.3
prosemirror-transform:
specifier: ^1.9.0
version: 1.9.0
prosemirror-view:
specifier: ^1.33.7
version: 1.33.7
qrcode:
specifier: ^1.5.3
version: 1.5.3
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-router:
specifier: ^6.16.0
version: 6.21.0(react@18.2.0)
react-router-dom:
specifier: ^6.16.0
version: 6.21.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-use:
specifier: ^17.4.0
version: 17.4.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
tailwind-merge:
specifier: ^1.14.0
version: 1.14.0
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.0(ts-node@10.9.2(@swc/core@1.3.101)(@types/node@20.10.5)(typescript@5.3.3)))
uniqolor:
specifier: ^1.1.0
version: 1.1.1
devDependencies:
'@types/react':
specifier: ^18.2.15
version: 18.2.45
'@types/react-dom':
specifier: ^18.2.7
version: 18.2.18
'@typescript-eslint/eslint-plugin':
specifier: ^6.0.0
version: 6.15.0(@typescript-eslint/parser@6.15.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser':
specifier: ^6.0.0
version: 6.15.0(eslint@8.56.0)(typescript@5.3.3)
'@vitejs/plugin-react-swc':
specifier: ^3.3.2
version: 3.5.0(vite@5.0.10(@types/node@20.10.5))
autoprefixer:
specifier: ^10.4.14
version: 10.4.16(postcss@8.4.32)
eslint:
specifier: ^8.45.0
version: 8.56.0
eslint-plugin-react-hooks:
specifier: ^4.6.0
version: 4.6.0(eslint@8.56.0)
eslint-plugin-react-refresh:
specifier: ^0.4.3
version: 0.4.5(eslint@8.56.0)
postcss:
specifier: ^8.4.27
version: 8.4.32
tailwindcss:
specifier: ^3.3.3
version: 3.4.0(ts-node@10.9.2(@swc/core@1.3.101)(@types/node@20.10.5)(typescript@5.3.3))
typescript:
specifier: ^5.0.2
version: 5.3.3
vite:
specifier: ^5.0.10
version: 5.0.10(@types/node@20.10.5)
examples/todo:
dependencies:
'@radix-ui/react-checkbox':
@@ -2251,6 +2375,9 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
@@ -3482,6 +3609,9 @@ packages:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
@@ -3696,6 +3826,48 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
prosemirror-commands@1.5.2:
resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==}
prosemirror-dropcursor@1.8.1:
resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==}
prosemirror-example-setup@1.2.2:
resolution: {integrity: sha512-pHJc656IgYm249RNp0eQaWNmnyWGk6OrzysWfYI4+NwE14HQ7YNYOlRBLErUS6uCAHIYJLNXf0/XCmf1OCtNbQ==}
prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
prosemirror-history@1.4.0:
resolution: {integrity: sha512-UUiGzDVcqo1lovOPdi9YxxUps3oBFWAIYkXLu3Ot+JPv1qzVogRbcizxK3LhHmtaUxclohgiOVesRw5QSlMnbQ==}
prosemirror-inputrules@1.4.0:
resolution: {integrity: sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==}
prosemirror-keymap@1.2.2:
resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==}
prosemirror-menu@1.2.4:
resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==}
prosemirror-model@1.21.1:
resolution: {integrity: sha512-IVBAuMqOfltTr7yPypwpfdGT+6rGAteVOw2FO6GEvCGGa1ZwxLseqC1Eax/EChDvG/xGquB2d/hLdgh3THpsYg==}
prosemirror-schema-basic@1.2.2:
resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==}
prosemirror-schema-list@1.3.0:
resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==}
prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
prosemirror-transform@1.9.0:
resolution: {integrity: sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==}
prosemirror-view@1.33.7:
resolution: {integrity: sha512-jo6eMQCtPRwcrA2jISBCnm0Dd2B+szS08BU1Ay+XGiozHo5EZMHfLQE8R5nO4vb1spTH2RW1woZIYXRiQsuP8g==}
proxy-agent@6.3.0:
resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==}
engines: {node: '>= 14'}
@@ -3889,6 +4061,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
rtl-css-js@1.16.1:
resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==}
@@ -4560,6 +4735,9 @@ packages:
vscode-textmate@8.0.0:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
wait-port@1.1.0:
resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==}
engines: {node: '>=10'}
@@ -6397,6 +6575,8 @@ snapshots:
create-require@1.1.1: {}
crelt@1.0.6: {}
cross-fetch@4.0.0:
dependencies:
node-fetch: 2.7.0
@@ -7725,6 +7905,8 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
orderedmap@2.1.1: {}
os-tmpdir@1.0.2: {}
outdent@0.5.0: {}
@@ -7925,6 +8107,91 @@ snapshots:
progress@2.0.3: {}
prosemirror-commands@1.5.2:
dependencies:
prosemirror-model: 1.21.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
prosemirror-dropcursor@1.8.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
prosemirror-view: 1.33.7
prosemirror-example-setup@1.2.2:
dependencies:
prosemirror-commands: 1.5.2
prosemirror-dropcursor: 1.8.1
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.4.0
prosemirror-inputrules: 1.4.0
prosemirror-keymap: 1.2.2
prosemirror-menu: 1.2.4
prosemirror-schema-list: 1.3.0
prosemirror-state: 1.4.3
prosemirror-gapcursor@1.3.2:
dependencies:
prosemirror-keymap: 1.2.2
prosemirror-model: 1.21.1
prosemirror-state: 1.4.3
prosemirror-view: 1.33.7
prosemirror-history@1.4.0:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
prosemirror-view: 1.33.7
rope-sequence: 1.3.4
prosemirror-inputrules@1.4.0:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
prosemirror-keymap@1.2.2:
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
prosemirror-menu@1.2.4:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.5.2
prosemirror-history: 1.4.0
prosemirror-state: 1.4.3
prosemirror-model@1.21.1:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.2:
dependencies:
prosemirror-model: 1.21.1
prosemirror-schema-list@1.3.0:
dependencies:
prosemirror-model: 1.21.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
prosemirror-state@1.4.3:
dependencies:
prosemirror-model: 1.21.1
prosemirror-transform: 1.9.0
prosemirror-view: 1.33.7
prosemirror-transform@1.9.0:
dependencies:
prosemirror-model: 1.21.1
prosemirror-view@1.33.7:
dependencies:
prosemirror-model: 1.21.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.9.0
proxy-agent@6.3.0:
dependencies:
agent-base: 7.1.0
@@ -8190,6 +8457,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.9.1
fsevents: 2.3.3
rope-sequence@1.3.4: {}
rtl-css-js@1.16.1:
dependencies:
'@babel/runtime': 7.23.6
@@ -8946,6 +9215,8 @@ snapshots:
vscode-textmate@8.0.0: {}
w3c-keyname@2.2.8: {}
wait-port@1.1.0:
dependencies:
chalk: 4.1.2