@datalayer/lexical-loro 0.0.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -138
- package/package.json +84 -53
- package/lib/DiffMerge.d.ts +0 -39
- package/lib/DiffMerge.js +0 -437
- package/lib/LoroCollaborativePlugin.d.ts +0 -62
- package/lib/LoroCollaborativePlugin.js +0 -2826
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -5
- package/lib/stableNodeState.d.ts +0 -8
- package/lib/stableNodeState.js +0 -15
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sponsors/datalayer)
|
|
4
4
|
|
|
5
|
-
# ✍️ 🦜
|
|
5
|
+
# ✍️ 🦜 Collaborative Plugin for Lexical with Loro CRDT
|
|
6
6
|
|
|
7
7
|
A collaborative editing plugin for [Lexical](https://github.com/facebook/lexical) Rich Editor built with [Loro](https://github.com/loro-dev) CRDT, providing real-time collaborative editing capabilities with conflict-free synchronization.
|
|
8
8
|
|
|
@@ -305,151 +305,38 @@ The examples include:
|
|
|
305
305
|
|
|
306
306
|
See `src/examples/README.md` for detailed example documentation.
|
|
307
307
|
|
|
308
|
-
## Project Structure
|
|
309
|
-
|
|
310
|
-
### Core Components
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
src/
|
|
314
|
-
├── LoroCollaborativePlugin.tsx # Main Lexical plugin for collaboration
|
|
315
|
-
└── vite-env.d.ts # TypeScript definitions
|
|
316
|
-
|
|
317
|
-
lexical_loro/ # Python WebSocket server package
|
|
318
|
-
├── __init__.py # Package exports
|
|
319
|
-
├── server.py # WebSocket server implementation
|
|
320
|
-
├── cli.py # Command line interface
|
|
321
|
-
├── model/
|
|
322
|
-
│ └── lexical_model.py # Standalone LexicalModel library
|
|
323
|
-
└── tests/ # Python test suite
|
|
324
|
-
|
|
325
|
-
docs/
|
|
326
|
-
└── LEXICAL_MODEL_GUIDE.md # Comprehensive library documentation
|
|
327
|
-
|
|
328
|
-
examples/
|
|
329
|
-
├── memory_only_example.py # Basic LexicalModel usage
|
|
330
|
-
├── file_sync_example.py # File persistence example
|
|
331
|
-
├── collaboration_example.py # Collaborative editing simulation
|
|
332
|
-
└── README.md # Examples documentation
|
|
333
|
-
|
|
334
|
-
pyproject.toml # Python package configuration
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Examples Directory
|
|
338
|
-
|
|
339
|
-
```
|
|
340
|
-
src/examples/ # Complete demo application
|
|
341
|
-
├── App.tsx # Demo app with dual editors
|
|
342
|
-
├── LexicalCollaborativeEditor.tsx # Rich text editor example
|
|
343
|
-
├── TextAreaCollaborativeEditor.tsx # Simple text editor example
|
|
344
|
-
├── ServerSelector.tsx # Server selection UI
|
|
345
|
-
├── LexicalToolbar.tsx # Rich text toolbar
|
|
346
|
-
├── main.tsx # Demo app entry point
|
|
347
|
-
└── *.css # Styling for examples
|
|
348
|
-
|
|
349
|
-
servers/
|
|
350
|
-
└── server.ts # Node.js server (for comparison)
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
### Archive
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
src/archive/ # Historical plugin implementations
|
|
357
|
-
├── LoroCollaborativePlugin0.tsx # Previous versions for reference
|
|
358
|
-
├── LoroCollaborativePlugin1.tsx
|
|
359
|
-
├── LoroCollaborativePlugin2.tsx
|
|
360
|
-
├── LoroCollaborativePlugin3.tsx
|
|
361
|
-
├── LoroCollaborativePlugin4.tsx
|
|
362
|
-
└── LoroCollaborativePlugin5.tsx
|
|
363
|
-
```
|
|
364
|
-
|
|
365
308
|
## Architecture
|
|
366
309
|
|
|
367
|
-
For detailed architecture documentation, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
368
|
-
|
|
369
|
-
### System Overview
|
|
370
|
-
|
|
371
|
-
The collaboration system consists of three main components:
|
|
372
|
-
|
|
373
|
-
1. **LoroCollaborativePlugin** (Client-side) - Lexical integration
|
|
374
|
-
2. **LoroWebSocketServer** (Server-side) - Real-time synchronization
|
|
375
|
-
3. **LexicalModel** (Standalone Library) - Independent document model
|
|
376
|
-
|
|
377
|
-
### Data Flow
|
|
378
|
-
|
|
379
310
|
```
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
311
|
+
EDITOR 1 EDITOR 2
|
|
312
|
+
|
|
313
|
+
loro loro
|
|
314
|
+
- node(data: root(1)) - node(data: root(12))
|
|
315
|
+
- node(data: element(2)) - node(data: element(22))
|
|
316
|
+
- node(data: text(3)) - node(data: text(13))
|
|
317
|
+
- node(data: counter(4)) - node(data: counter(4))
|
|
318
|
+
|
|
319
|
+
<---- loro updates via websocket ------>
|
|
320
|
+
<---- loro node ids are the same ------>
|
|
321
|
+
<---- lexical node keys are different ------>
|
|
322
|
+
|
|
323
|
+
lexical lexical
|
|
324
|
+
- root(1) - root(12)
|
|
325
|
+
- element(2) - element(22)
|
|
326
|
+
- text(3) - text(13)
|
|
327
|
+
- counter(4) - counter(49)
|
|
383
328
|
```
|
|
384
329
|
|
|
385
|
-
##
|
|
386
|
-
|
|
387
|
-
For detailed configuration options, see [`docs/API.md`](docs/API.md).
|
|
388
|
-
|
|
389
|
-
### Quick Configuration
|
|
390
|
-
|
|
391
|
-
```tsx
|
|
392
|
-
// Plugin configuration
|
|
393
|
-
<LoroCollaborativePlugin
|
|
394
|
-
websocketUrl="ws://localhost:8081"
|
|
395
|
-
docId="my-document"
|
|
396
|
-
username="user123"
|
|
397
|
-
debug={true}
|
|
398
|
-
/>
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
```bash
|
|
402
|
-
# Server configuration
|
|
403
|
-
lexical-loro-server --port 8081 --log-level DEBUG
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
## Development
|
|
407
|
-
|
|
408
|
-
For comprehensive development guidelines, see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
|
|
409
|
-
|
|
410
|
-
### Quick Start
|
|
411
|
-
|
|
412
|
-
```bash
|
|
413
|
-
# Install dependencies
|
|
414
|
-
npm install
|
|
415
|
-
pip install -e ".[dev]"
|
|
416
|
-
|
|
417
|
-
# Run tests
|
|
418
|
-
npm test
|
|
419
|
-
npm run test:py
|
|
420
|
-
|
|
421
|
-
# Start development server
|
|
422
|
-
lexical-loro-server --log-level DEBUG
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
## Contributing
|
|
426
|
-
|
|
427
|
-
We welcome contributions! Please see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for detailed guidelines.
|
|
428
|
-
|
|
429
|
-
### Quick Contributing Guide
|
|
430
|
-
|
|
431
|
-
1. Fork the repository
|
|
432
|
-
2. Create a feature branch
|
|
433
|
-
3. Focus changes on core components
|
|
434
|
-
4. Add tests for new functionality
|
|
435
|
-
5. Update documentation as needed
|
|
436
|
-
6. Submit a pull request
|
|
330
|
+
## Examples
|
|
437
331
|
|
|
438
|
-
|
|
332
|
+
Loro examples
|
|
439
333
|
|
|
440
|
-
-
|
|
441
|
-
- **[Initialization Guide](docs/INITIALIZATION_GUIDE.md)** - Best practices for setup
|
|
442
|
-
- **[Architecture](docs/ARCHITECTURE.md)** - System design and data flow
|
|
443
|
-
- **[Development Guide](docs/DEVELOPMENT.md)** - Contributing and development setup
|
|
444
|
-
- **[LexicalModel Guide](docs/LEXICAL_MODEL_GUIDE.md)** - Standalone library documentation
|
|
334
|
+
- http://localhost:3000/?isCollab=true
|
|
445
335
|
|
|
446
|
-
|
|
336
|
+
- http://localhost:3000/split/?isCollab=true
|
|
447
337
|
|
|
448
|
-
|
|
338
|
+
Y.js examples (for reference)
|
|
449
339
|
|
|
450
|
-
|
|
340
|
+
- http://localhost:3000/?isCollab=true&useYjs=true
|
|
451
341
|
|
|
452
|
-
-
|
|
453
|
-
- [Lexical](https://lexical.dev/) - Facebook's extensible text editor framework
|
|
454
|
-
- [React](https://reactjs.org/) - UI library for plugin hooks
|
|
455
|
-
- [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) - Real-time communication
|
|
342
|
+
- http://localhost:3000/split/?isCollab=true&useYjs=true
|
package/package.json
CHANGED
|
@@ -1,93 +1,124 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/lexical-loro",
|
|
3
|
-
"
|
|
4
|
-
"version": "0.0.7",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Collaborative editing solution for Lexical based on Loro CRDT",
|
|
3
|
+
"version": "0.2.1",
|
|
7
4
|
"main": "lib/index.js",
|
|
8
|
-
"types": "lib/index.d.ts",
|
|
9
5
|
"files": [
|
|
10
|
-
"lib
|
|
6
|
+
"lib/**/*.{css,d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
7
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
8
|
+
"schema/*.json"
|
|
11
9
|
],
|
|
12
10
|
"keywords": [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"collaborative",
|
|
17
|
-
"editor",
|
|
18
|
-
"plugin",
|
|
19
|
-
"react",
|
|
20
|
-
"rich-editor"
|
|
11
|
+
"jupyter",
|
|
12
|
+
"jupyterlab",
|
|
13
|
+
"react"
|
|
21
14
|
],
|
|
22
15
|
"repository": {
|
|
23
16
|
"type": "git",
|
|
24
17
|
"url": "https://github.com/datalayer/lexical-loro.git"
|
|
25
18
|
},
|
|
26
|
-
"
|
|
27
|
-
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"style/*.css",
|
|
21
|
+
"src/**/*.css",
|
|
22
|
+
"style/index.js"
|
|
23
|
+
],
|
|
24
|
+
"styleModule": "style/index.js",
|
|
28
25
|
"publishConfig": {
|
|
29
26
|
"access": "public"
|
|
30
27
|
},
|
|
31
28
|
"scripts": {
|
|
32
29
|
"build": "tsc -b && vite build",
|
|
30
|
+
"dev": "cross-env NODE_ENV=development concurrently \"npm:server:py:ws\" \"npm:server:py:mcp\" \"vite --host --port 3000\"",
|
|
31
|
+
"build:tsc": "tsc",
|
|
32
|
+
"build:dev": "vite build",
|
|
33
|
+
"build:prod": "vite build --mode production",
|
|
34
|
+
"server:loro": "fkill -s :1235 && cross-env HOST=localhost PORT=1235 YPERSISTENCE=./.wss-loro tsx src/collab/loro/servers/ws/server.ts",
|
|
35
|
+
"server:yjs": "fkill -s :1234 && cross-env HOST=localhost PORT=1234 YPERSISTENCE=./.wss-yjs tsx src/collab/yjs/servers/ws/server.ts",
|
|
36
|
+
"server:yjs:dist": "cross-env HOST=localhost PORT=1234 YPERSISTENCE=./.yjs-wss-db npx y-websocket",
|
|
37
|
+
"test": "npm run test:js && npm run test:py",
|
|
38
|
+
"test:watch": "jest --watch",
|
|
39
|
+
"test:coverage": "npm run test:js:coverage && npm run test:py:coverage",
|
|
40
|
+
"test:js:coverage": "jest --coverage",
|
|
41
|
+
"preview": "vite preview",
|
|
42
|
+
"watch:lib": "tsc -b -w",
|
|
33
43
|
"clean": "rimraf dist lib",
|
|
34
|
-
"dev": "npm run example",
|
|
35
44
|
"dev:all:js": "npm run example:js",
|
|
36
45
|
"dev:all:py": "npm run example:py",
|
|
37
46
|
"dev:vite": "npm run example:vite",
|
|
38
|
-
"example": "concurrently \"npm run server\" \"npm run server:py\" \"npm run server:mcp\" \"npm run example:vite\"",
|
|
47
|
+
"example": "concurrently \"npm run server\" \"npm run server:py\" \"npm run server:py:mcp\" \"npm run example:vite\"",
|
|
39
48
|
"example:js": "concurrently \"npm run server\" \"npm run example:vite\"",
|
|
40
49
|
"example:py": "concurrently \"npm run server:py:dev\" \"npm run example:vite\"",
|
|
41
50
|
"example:vite": "vite",
|
|
42
51
|
"lint": "eslint .",
|
|
43
|
-
"preview": "vite preview",
|
|
44
52
|
"server": "tsx dev/node-server.ts",
|
|
45
53
|
"server:py": "lexical-loro-server",
|
|
46
54
|
"server:py:dev": "python3 -m lexical_loro.cli",
|
|
47
|
-
"server:
|
|
48
|
-
"
|
|
55
|
+
"server:py:ws": "python3 -m lexical_loro.cli --host localhost --port 3002 --autosave-interval 5",
|
|
56
|
+
"server:py:mcp": "python3 -m lexical_loro.mcp start --transport streamable-http --host 0.0.0.0 --port 3001 --websocket-url ws://localhost:3002 --documents-path ./documents --log-level DEBUG",
|
|
49
57
|
"test:js": "vitest run",
|
|
50
|
-
"test:py": "python3 -m pytest
|
|
51
|
-
"test:py:coverage": "python3 -m pytest
|
|
52
|
-
"test:py:watch": "python3 -m pytest
|
|
53
|
-
"watch:lib": "tsc -b -w"
|
|
58
|
+
"test:py": "python3 -m pytest tests/ -v",
|
|
59
|
+
"test:py:coverage": "python3 -m pytest tests/ --cov=lexical_loro --cov-report=html --cov-report=term",
|
|
60
|
+
"test:py:watch": "python3 -m pytest tests/ -v --tb=short -f"
|
|
54
61
|
},
|
|
55
62
|
"dependencies": {
|
|
56
|
-
"
|
|
57
|
-
"react": "^
|
|
58
|
-
"
|
|
59
|
-
"lexical": "^0.
|
|
60
|
-
"@lexical/
|
|
61
|
-
"@lexical/
|
|
63
|
+
"@datalayer/primer-addons": "^1.0.8",
|
|
64
|
+
"@floating-ui/react": "^0.27.8",
|
|
65
|
+
"@lexical/clipboard": "^0.35.0",
|
|
66
|
+
"@lexical/code": "^0.35.0",
|
|
67
|
+
"@lexical/code-shiki": "^0.35.0",
|
|
68
|
+
"@lexical/file": "^0.35.0",
|
|
69
|
+
"@lexical/hashtag": "^0.35.0",
|
|
70
|
+
"@lexical/headless": "^0.35.0",
|
|
71
|
+
"@lexical/link": "^0.35.0",
|
|
72
|
+
"@lexical/list": "^0.35.0",
|
|
73
|
+
"@lexical/mark": "^0.35.0",
|
|
74
|
+
"@lexical/overflow": "^0.35.0",
|
|
75
|
+
"@lexical/plain-text": "^0.35.0",
|
|
76
|
+
"@lexical/react": "^0.35.0",
|
|
77
|
+
"@lexical/rich-text": "^0.35.0",
|
|
78
|
+
"@lexical/selection": "^0.35.0",
|
|
79
|
+
"@lexical/table": "^0.35.0",
|
|
80
|
+
"@lexical/utils": "^0.35.0",
|
|
81
|
+
"date-fns": "^4.1.0",
|
|
82
|
+
"katex": "^0.16.10",
|
|
83
|
+
"lexical": "^0.35.0",
|
|
84
|
+
"lodash-es": "^4.17.21",
|
|
85
|
+
"loro-crdt": "^1.8.0",
|
|
86
|
+
"prettier": "^2.8.8",
|
|
87
|
+
"react": "^18.2.0",
|
|
88
|
+
"react-day-picker": "^9.7.0",
|
|
89
|
+
"react-dom": "^18.2.0",
|
|
90
|
+
"react-error-boundary": "^3.1.4",
|
|
91
|
+
"y-websocket": "3.0.0",
|
|
92
|
+
"yjs": ">=13.5.42"
|
|
62
93
|
},
|
|
63
94
|
"devDependencies": {
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
-
"@
|
|
67
|
-
"@
|
|
68
|
-
"@
|
|
95
|
+
"@babel/plugin-transform-flow-strip-types": "^7.24.7",
|
|
96
|
+
"@babel/preset-react": "^7.24.7",
|
|
97
|
+
"@excalidraw/excalidraw": "^0.18.0",
|
|
98
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
99
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
100
|
+
"@types/jest": "^29.5.12",
|
|
101
|
+
"@types/lodash-es": "^4.14.182",
|
|
102
|
+
"@types/prettier": "^3.0.0",
|
|
103
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
69
104
|
"concurrently": "^9.2.0",
|
|
105
|
+
"cross-env": "^7.0.3",
|
|
70
106
|
"eslint": "^9.30.1",
|
|
71
107
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
72
108
|
"eslint-plugin-react-refresh": "^0.4.20",
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"tsx": "^4.20.3",
|
|
85
|
-
"typescript": "~5.8.3",
|
|
86
|
-
"typescript-eslint": "^8.35.1",
|
|
87
|
-
"vite": "^7.0.4",
|
|
88
|
-
"vitest": "^2.1.4",
|
|
109
|
+
"fkill-cli": "^8.0.0",
|
|
110
|
+
"jest": "^29.7.0",
|
|
111
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
112
|
+
"prettier": "^3.3.2",
|
|
113
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
114
|
+
"ts-jest": "^29.1.2",
|
|
115
|
+
"ts-node": "^10.9.2",
|
|
116
|
+
"tsx": "^4.20.5",
|
|
117
|
+
"typescript": "^5.9.2",
|
|
118
|
+
"vite": "^7.0.0",
|
|
119
|
+
"vite-plugin-static-copy": "^3.1.2",
|
|
89
120
|
"vite-plugin-top-level-await": "^1.6.0",
|
|
90
121
|
"vite-plugin-wasm": "^3.5.0",
|
|
91
|
-
"
|
|
122
|
+
"vitest": "^3.2.4"
|
|
92
123
|
}
|
|
93
124
|
}
|
package/lib/DiffMerge.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type LexicalEditor } from 'lexical';
|
|
2
|
-
/**
|
|
3
|
-
* DiffMerge System for Lexical Editor
|
|
4
|
-
*
|
|
5
|
-
* This module provides sophisticated differential updates for Lexical editor states,
|
|
6
|
-
* preventing wholesale state replacement that would destroy React decorator nodes
|
|
7
|
-
* like YouTube embeds, counters, and other custom components.
|
|
8
|
-
*
|
|
9
|
-
* Key Features:
|
|
10
|
-
* - Selective node updates (only changed content)
|
|
11
|
-
* - Decorator node preservation (YouTube, Counter nodes remain untouched)
|
|
12
|
-
* - Table structure support with cell-level updates
|
|
13
|
-
* - Graceful fallback for unsupported operations
|
|
14
|
-
* - Deep content comparison to minimize unnecessary updates
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const success = applyDifferentialUpdate(editor, newState, 'collaboration');
|
|
19
|
-
* if (!success) {
|
|
20
|
-
* // Fall back to setEditorState if differential update fails
|
|
21
|
-
* editor.setEditorState(newState);
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
interface EditorStateData {
|
|
26
|
-
root: {
|
|
27
|
-
type: 'root';
|
|
28
|
-
children: any[];
|
|
29
|
-
direction?: string | null;
|
|
30
|
-
format?: number;
|
|
31
|
-
indent?: number;
|
|
32
|
-
version?: number;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Main function to apply differential updates to the editor
|
|
37
|
-
*/
|
|
38
|
-
export declare function applyDifferentialUpdate(editor: LexicalEditor, newStateData: EditorStateData | any, source?: string): boolean;
|
|
39
|
-
export {};
|