@collaflow/markdown 0.1.0
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/LICENSE +201 -0
- package/README.md +114 -0
- package/dist/index.cjs +708 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +238 -0
- package/dist/index.d.ts +238 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/editor/factory.ts
|
|
2
|
+
import * as Y4 from "yjs";
|
|
3
|
+
import { Editor, defaultValueCtx, editorViewCtx, rootCtx } from "@milkdown/core";
|
|
4
|
+
import { commonmark } from "@milkdown/preset-commonmark";
|
|
5
|
+
import { listener, listenerCtx as listenerCtx2 } from "@milkdown/plugin-listener";
|
|
6
|
+
import { getMarkdown, replaceAll as replaceAll2 } from "@milkdown/utils";
|
|
7
|
+
import { block } from "@milkdown/plugin-block";
|
|
8
|
+
|
|
9
|
+
// src/editor/options.ts
|
|
10
|
+
var DEFAULT_EDITOR_OPTIONS = {
|
|
11
|
+
defaultValue: "",
|
|
12
|
+
highlight: true,
|
|
13
|
+
theme: "light"
|
|
14
|
+
};
|
|
15
|
+
function resolveEditorOptions(options) {
|
|
16
|
+
return {
|
|
17
|
+
root: options.root,
|
|
18
|
+
defaultValue: options.defaultValue ?? DEFAULT_EDITOR_OPTIONS.defaultValue,
|
|
19
|
+
highlight: options.highlight ?? DEFAULT_EDITOR_OPTIONS.highlight,
|
|
20
|
+
theme: options.theme ?? DEFAULT_EDITOR_OPTIONS.theme,
|
|
21
|
+
collab: options.collab,
|
|
22
|
+
plugins: options.plugins,
|
|
23
|
+
onChange: options.onChange,
|
|
24
|
+
onAwarenessChange: options.onAwarenessChange
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/collab/provider.ts
|
|
29
|
+
import * as Y2 from "yjs";
|
|
30
|
+
import * as awarenessProtocol from "y-protocols/awareness";
|
|
31
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
32
|
+
|
|
33
|
+
// src/collab/cursor.ts
|
|
34
|
+
import * as Y from "yjs";
|
|
35
|
+
function toBase64(bytes) {
|
|
36
|
+
let binary = "";
|
|
37
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
38
|
+
return btoa(binary);
|
|
39
|
+
}
|
|
40
|
+
function fromBase64(text) {
|
|
41
|
+
const binary = atob(text);
|
|
42
|
+
const bytes = new Uint8Array(binary.length);
|
|
43
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
function encodeRelativePosition2(yText, index) {
|
|
47
|
+
const rel = Y.createRelativePositionFromTypeIndex(yText, index);
|
|
48
|
+
return toBase64(Y.encodeRelativePosition(rel));
|
|
49
|
+
}
|
|
50
|
+
function decodeOffset(yText, encoded) {
|
|
51
|
+
try {
|
|
52
|
+
const rel = Y.decodeRelativePosition(fromBase64(encoded));
|
|
53
|
+
const doc = yText.doc;
|
|
54
|
+
if (!doc) return null;
|
|
55
|
+
const abs = Y.createAbsolutePositionFromRelativePosition(rel, doc);
|
|
56
|
+
if (!abs || abs.type !== yText) return null;
|
|
57
|
+
return abs.index;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function decodeSelection(yText, selection) {
|
|
63
|
+
const anchor = decodeOffset(yText, selection.anchor);
|
|
64
|
+
const head = decodeOffset(yText, selection.head);
|
|
65
|
+
if (anchor === null || head === null) return null;
|
|
66
|
+
return { anchor, head };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/collab/provider.ts
|
|
70
|
+
var CollaFlowProvider = class {
|
|
71
|
+
doc;
|
|
72
|
+
awareness;
|
|
73
|
+
provider;
|
|
74
|
+
connected = false;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.doc = new Y2.Doc();
|
|
77
|
+
this.awareness = new awarenessProtocol.Awareness(this.doc);
|
|
78
|
+
if (options.user) {
|
|
79
|
+
this.awareness.setLocalState({ user: options.user });
|
|
80
|
+
}
|
|
81
|
+
this.provider = new HocuspocusProvider({
|
|
82
|
+
url: options.serverUrl,
|
|
83
|
+
name: options.roomId,
|
|
84
|
+
document: this.doc,
|
|
85
|
+
awareness: this.awareness,
|
|
86
|
+
onStatus: ({ status }) => {
|
|
87
|
+
this.connected = status === "connected";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 当前是否已与协同服务建立连接。
|
|
93
|
+
*/
|
|
94
|
+
get isConnected() {
|
|
95
|
+
return this.connected;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 协同内容源:以 Markdown 文本为唯一真相的 Y.Text。
|
|
99
|
+
*
|
|
100
|
+
* 源码区与预览区都读写这一份文本;协同(Hocuspocus)只负责同步它,
|
|
101
|
+
* 因此多人编辑按字符级 CRDT 合并,不会再出现「整体替换」式的覆盖。
|
|
102
|
+
*/
|
|
103
|
+
get content() {
|
|
104
|
+
return this.doc.getText("content");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 获取当前在线用户列表。
|
|
108
|
+
*/
|
|
109
|
+
getAwarenessUsers() {
|
|
110
|
+
const users = [];
|
|
111
|
+
this.awareness.getStates().forEach((state, clientId) => {
|
|
112
|
+
const user = state?.user;
|
|
113
|
+
if (user?.name && user?.color) {
|
|
114
|
+
users.push({ clientId, user });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return users;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 建立连接。
|
|
121
|
+
*/
|
|
122
|
+
connect() {
|
|
123
|
+
this.provider.connect();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 上报本地选区(以 Y.Text 字符偏移表示)。
|
|
127
|
+
*
|
|
128
|
+
* 会被编码为相对位置存入 awareness,随并发编辑自愈,避免光标错位。
|
|
129
|
+
*/
|
|
130
|
+
setLocalSelection(anchor, head) {
|
|
131
|
+
const selection = {
|
|
132
|
+
anchor: encodeRelativePosition2(this.content, anchor),
|
|
133
|
+
head: encodeRelativePosition2(this.content, head)
|
|
134
|
+
};
|
|
135
|
+
this.awareness.setLocalStateField("selection", selection);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 读取远端用户的光标列表(已解码为 Y.Text 绝对偏移)。
|
|
139
|
+
*/
|
|
140
|
+
getRemoteCursors() {
|
|
141
|
+
const cursors = [];
|
|
142
|
+
this.awareness.getStates().forEach((state, clientId) => {
|
|
143
|
+
if (clientId === this.doc.clientID) return;
|
|
144
|
+
const user = state?.user;
|
|
145
|
+
const selection = state?.selection;
|
|
146
|
+
if (!user?.name || !user?.color || !selection) return;
|
|
147
|
+
const offsets = decodeSelection(this.content, selection);
|
|
148
|
+
if (!offsets) return;
|
|
149
|
+
cursors.push({ clientId, user, anchor: offsets.anchor, head: offsets.head });
|
|
150
|
+
});
|
|
151
|
+
return cursors;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 断开连接(保留本地 doc 内容,可再次 connect)。
|
|
155
|
+
*/
|
|
156
|
+
disconnect() {
|
|
157
|
+
this.provider.disconnect();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 销毁 Provider,清理连接与 Yjs 资源。
|
|
161
|
+
*/
|
|
162
|
+
destroy() {
|
|
163
|
+
this.provider.destroy();
|
|
164
|
+
this.awareness.destroy();
|
|
165
|
+
this.doc.destroy();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/collab/yjs-binding.ts
|
|
170
|
+
import "yjs";
|
|
171
|
+
import { InitReady, editorCtx } from "@milkdown/core";
|
|
172
|
+
import { listenerCtx } from "@milkdown/plugin-listener";
|
|
173
|
+
import { replaceAll } from "@milkdown/utils";
|
|
174
|
+
var YJS_ORIGIN_MILKDOWN = "colla-md:milkdown";
|
|
175
|
+
var YJS_ORIGIN_TEXTAREA = "colla-md:textarea";
|
|
176
|
+
function diffApply(yText, newText, origin) {
|
|
177
|
+
const oldText = yText.toString();
|
|
178
|
+
if (oldText === newText) return;
|
|
179
|
+
let start = 0;
|
|
180
|
+
const minLen = Math.min(oldText.length, newText.length);
|
|
181
|
+
while (start < minLen && oldText[start] === newText[start]) start++;
|
|
182
|
+
let endOld = oldText.length;
|
|
183
|
+
let endNew = newText.length;
|
|
184
|
+
while (endOld > start && endNew > start && oldText[endOld - 1] === newText[endNew - 1]) {
|
|
185
|
+
endOld--;
|
|
186
|
+
endNew--;
|
|
187
|
+
}
|
|
188
|
+
const deleteLength = endOld - start;
|
|
189
|
+
const insertText = newText.slice(start, endNew);
|
|
190
|
+
const ydoc = yText.doc;
|
|
191
|
+
if (!ydoc) return;
|
|
192
|
+
ydoc.transact(() => {
|
|
193
|
+
if (deleteLength > 0) yText.delete(start, deleteLength);
|
|
194
|
+
if (insertText) yText.insert(start, insertText);
|
|
195
|
+
}, origin);
|
|
196
|
+
}
|
|
197
|
+
function unescapeLeadingHash(markdown2) {
|
|
198
|
+
return markdown2.replace(/^\\#/gm, "#");
|
|
199
|
+
}
|
|
200
|
+
function createCollabPlugins(content) {
|
|
201
|
+
return (ctx) => async () => {
|
|
202
|
+
await ctx.wait(InitReady);
|
|
203
|
+
const editor = ctx.get(editorCtx);
|
|
204
|
+
content.observe((_, transaction) => {
|
|
205
|
+
if (transaction.origin === YJS_ORIGIN_MILKDOWN) return;
|
|
206
|
+
void editor.action(replaceAll(content.toString()));
|
|
207
|
+
});
|
|
208
|
+
ctx.get(listenerCtx).markdownUpdated((_, markdown2) => {
|
|
209
|
+
diffApply(content, unescapeLeadingHash(markdown2), YJS_ORIGIN_MILKDOWN);
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/editor/plugins/awareness.ts
|
|
215
|
+
import { yCursorPlugin } from "y-prosemirror";
|
|
216
|
+
|
|
217
|
+
// src/editor/plugins/highlight.ts
|
|
218
|
+
import { refractor } from "refractor";
|
|
219
|
+
import { prism, prismConfig } from "@milkdown/plugin-prism";
|
|
220
|
+
import bash from "refractor/bash";
|
|
221
|
+
import css from "refractor/css";
|
|
222
|
+
import javascript from "refractor/javascript";
|
|
223
|
+
import json from "refractor/json";
|
|
224
|
+
import jsx from "refractor/jsx";
|
|
225
|
+
import markdown from "refractor/markdown";
|
|
226
|
+
import markup from "refractor/markup";
|
|
227
|
+
import python from "refractor/python";
|
|
228
|
+
import tsx from "refractor/tsx";
|
|
229
|
+
import typescript from "refractor/typescript";
|
|
230
|
+
var DEFAULT_SYNTAXES = [
|
|
231
|
+
javascript,
|
|
232
|
+
typescript,
|
|
233
|
+
tsx,
|
|
234
|
+
jsx,
|
|
235
|
+
markup,
|
|
236
|
+
css,
|
|
237
|
+
markdown,
|
|
238
|
+
json,
|
|
239
|
+
bash,
|
|
240
|
+
python
|
|
241
|
+
];
|
|
242
|
+
function configureHighlightLanguages(syntaxes) {
|
|
243
|
+
for (const syntax of syntaxes) {
|
|
244
|
+
refractor.register(syntax);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function createHighlightPlugin(options) {
|
|
248
|
+
const isShiki = options?.type === "shiki";
|
|
249
|
+
if (isShiki) {
|
|
250
|
+
return { plugins: prism };
|
|
251
|
+
}
|
|
252
|
+
configureHighlightLanguages(DEFAULT_SYNTAXES);
|
|
253
|
+
return {
|
|
254
|
+
plugins: prism,
|
|
255
|
+
configure: (ctx) => {
|
|
256
|
+
ctx.set(prismConfig.key, { configureRefractor: (r) => r });
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/editor/plugins/layout.ts
|
|
262
|
+
import { $node } from "@milkdown/utils";
|
|
263
|
+
var emptyMarkdownSpec = {
|
|
264
|
+
parseMarkdown: {
|
|
265
|
+
match: () => false,
|
|
266
|
+
runner: () => {
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
toMarkdown: {
|
|
270
|
+
match: () => false,
|
|
271
|
+
runner: () => {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var calloutNode = $node("callout", () => ({
|
|
276
|
+
group: "block",
|
|
277
|
+
content: "block+",
|
|
278
|
+
attrs: {
|
|
279
|
+
type: { default: "info" }
|
|
280
|
+
},
|
|
281
|
+
parseDOM: [
|
|
282
|
+
{
|
|
283
|
+
tag: "div.callout",
|
|
284
|
+
getAttrs: (dom) => {
|
|
285
|
+
const element = dom;
|
|
286
|
+
return { type: element.dataset.type ?? "info" };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
toDOM: (node) => [
|
|
291
|
+
"div",
|
|
292
|
+
{ class: `callout callout--${node.attrs.type}`, "data-type": node.attrs.type },
|
|
293
|
+
0
|
|
294
|
+
],
|
|
295
|
+
...emptyMarkdownSpec
|
|
296
|
+
}));
|
|
297
|
+
var columnsNode = $node("columns", () => ({
|
|
298
|
+
group: "block",
|
|
299
|
+
content: "column+",
|
|
300
|
+
attrs: {
|
|
301
|
+
columns: { default: 2 }
|
|
302
|
+
},
|
|
303
|
+
parseDOM: [
|
|
304
|
+
{
|
|
305
|
+
tag: "div.columns",
|
|
306
|
+
getAttrs: (dom) => {
|
|
307
|
+
const element = dom;
|
|
308
|
+
return { columns: Number(element.dataset.columns) || 2 };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
toDOM: (node) => ["div", { class: "columns", "data-columns": node.attrs.columns }, 0],
|
|
313
|
+
...emptyMarkdownSpec
|
|
314
|
+
}));
|
|
315
|
+
var columnNode = $node("column", () => ({
|
|
316
|
+
group: "block",
|
|
317
|
+
content: "block+",
|
|
318
|
+
parseDOM: [{ tag: "div.column" }],
|
|
319
|
+
toDOM: () => ["div", { class: "column" }, 0],
|
|
320
|
+
...emptyMarkdownSpec
|
|
321
|
+
}));
|
|
322
|
+
var cardNode = $node("card", () => ({
|
|
323
|
+
group: "block",
|
|
324
|
+
content: "block+",
|
|
325
|
+
attrs: {
|
|
326
|
+
title: { default: "" }
|
|
327
|
+
},
|
|
328
|
+
parseDOM: [
|
|
329
|
+
{
|
|
330
|
+
tag: "div.card",
|
|
331
|
+
getAttrs: (dom) => {
|
|
332
|
+
const element = dom;
|
|
333
|
+
return { title: element.dataset.title ?? "" };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
toDOM: (node) => ["div", { class: "card", "data-title": node.attrs.title }, 0],
|
|
338
|
+
...emptyMarkdownSpec
|
|
339
|
+
}));
|
|
340
|
+
var layoutNodes = [
|
|
341
|
+
calloutNode,
|
|
342
|
+
columnsNode,
|
|
343
|
+
columnNode,
|
|
344
|
+
cardNode
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
// src/editor/plugins/theme.ts
|
|
348
|
+
import { EditorViewReady, rootDOMCtx } from "@milkdown/core";
|
|
349
|
+
import { lightTheme, darkTheme } from "@collaflow/design";
|
|
350
|
+
|
|
351
|
+
// src/styles/notion-style.ts
|
|
352
|
+
var NOTION_FONT_FAMILY = 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif';
|
|
353
|
+
var NOTION_MONO_FAMILY = 'SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
|
|
354
|
+
var NOTION_EDITOR_CSS = `
|
|
355
|
+
.colla-md {
|
|
356
|
+
font-family: ${NOTION_FONT_FAMILY};
|
|
357
|
+
font-size: 16px;
|
|
358
|
+
line-height: 1.5;
|
|
359
|
+
color: var(--cf-text-primary);
|
|
360
|
+
font-weight: 400;
|
|
361
|
+
word-break: break-word;
|
|
362
|
+
-webkit-font-smoothing: antialiased;
|
|
363
|
+
}
|
|
364
|
+
.colla-md h1 { font-size: 1.875em; font-weight: 700; line-height: 1.3; margin: 1.4em 0 0.2em; }
|
|
365
|
+
.colla-md h2 { font-size: 1.5em; font-weight: 600; line-height: 1.3; margin: 1.2em 0 0.2em; }
|
|
366
|
+
.colla-md h3 { font-size: 1.25em; font-weight: 600; line-height: 1.3; margin: 1em 0 0.2em; }
|
|
367
|
+
.colla-md h4 { font-size: 1em; font-weight: 600; line-height: 1.3; margin: 0.8em 0 0.2em; }
|
|
368
|
+
.colla-md h5, .colla-md h6 { font-size: 0.875em; font-weight: 600; line-height: 1.3; margin: 0.8em 0 0.2em; }
|
|
369
|
+
.colla-md p { margin: 0; padding: 3px 2px; line-height: 1.5; }
|
|
370
|
+
.colla-md ul, .colla-md ol { margin: 0; padding-left: 1.6em; padding-top: 2px; padding-bottom: 2px; }
|
|
371
|
+
.colla-md li { margin: 2px 0; }
|
|
372
|
+
.colla-md li > p { padding: 0; }
|
|
373
|
+
.colla-md :not(pre) > code {
|
|
374
|
+
font-family: ${NOTION_MONO_FAMILY};
|
|
375
|
+
background: rgba(135, 131, 120, 0.15);
|
|
376
|
+
color: var(--cf-text-primary);
|
|
377
|
+
border-radius: 4px;
|
|
378
|
+
padding: 0.2em 0.4em;
|
|
379
|
+
font-size: 0.85em;
|
|
380
|
+
}
|
|
381
|
+
.colla-md pre {
|
|
382
|
+
font-family: ${NOTION_MONO_FAMILY};
|
|
383
|
+
background: var(--cf-bg-tertiary);
|
|
384
|
+
color: var(--cf-text-primary);
|
|
385
|
+
border-radius: 4px;
|
|
386
|
+
padding: 1em 1.2em;
|
|
387
|
+
margin: 4px 0;
|
|
388
|
+
overflow: auto;
|
|
389
|
+
font-size: 0.85em;
|
|
390
|
+
line-height: 1.45;
|
|
391
|
+
}
|
|
392
|
+
.colla-md pre code { font-family: inherit; background: none; padding: 0; }
|
|
393
|
+
.colla-md blockquote {
|
|
394
|
+
border-left: 3px solid var(--cf-border-strong);
|
|
395
|
+
padding-left: 12px;
|
|
396
|
+
margin: 4px 0;
|
|
397
|
+
color: var(--cf-text-secondary);
|
|
398
|
+
}
|
|
399
|
+
.colla-md a { color: var(--cf-accent-blue); text-decoration: none; }
|
|
400
|
+
.colla-md a:hover { text-decoration: underline; }
|
|
401
|
+
.colla-md strong { font-weight: 600; }
|
|
402
|
+
.colla-md hr { border: none; border-top: 1px solid var(--cf-border-default); margin: 6px 0; }
|
|
403
|
+
.colla-md img { max-width: 100%; border-radius: 4px; }
|
|
404
|
+
`;
|
|
405
|
+
var COLLAB_CURSOR_CSS = `
|
|
406
|
+
.colla-rc-cursor {
|
|
407
|
+
position: absolute;
|
|
408
|
+
width: 2px;
|
|
409
|
+
pointer-events: none;
|
|
410
|
+
z-index: 20;
|
|
411
|
+
}
|
|
412
|
+
.colla-rc-label {
|
|
413
|
+
position: absolute;
|
|
414
|
+
top: -1.25em;
|
|
415
|
+
left: -1px;
|
|
416
|
+
font-size: 11px;
|
|
417
|
+
line-height: 1;
|
|
418
|
+
padding: 2px 5px;
|
|
419
|
+
border-radius: 3px 3px 3px 0;
|
|
420
|
+
color: #fff;
|
|
421
|
+
white-space: nowrap;
|
|
422
|
+
font-family: ${NOTION_FONT_FAMILY};
|
|
423
|
+
pointer-events: none;
|
|
424
|
+
user-select: none;
|
|
425
|
+
z-index: 21;
|
|
426
|
+
}
|
|
427
|
+
`;
|
|
428
|
+
var NOTION_STYLE_ID = "colla-md-notion-style";
|
|
429
|
+
var COLLAB_CURSOR_STYLE_ID = "colla-md-collab-cursor";
|
|
430
|
+
function injectNotionStyle() {
|
|
431
|
+
if (document.getElementById(NOTION_STYLE_ID)) return;
|
|
432
|
+
const style = document.createElement("style");
|
|
433
|
+
style.id = NOTION_STYLE_ID;
|
|
434
|
+
style.textContent = NOTION_EDITOR_CSS;
|
|
435
|
+
document.head.appendChild(style);
|
|
436
|
+
}
|
|
437
|
+
function injectCollabCursorStyle() {
|
|
438
|
+
if (document.getElementById(COLLAB_CURSOR_STYLE_ID)) return;
|
|
439
|
+
const style = document.createElement("style");
|
|
440
|
+
style.id = COLLAB_CURSOR_STYLE_ID;
|
|
441
|
+
style.textContent = COLLAB_CURSOR_CSS;
|
|
442
|
+
document.head.appendChild(style);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/editor/plugins/theme.ts
|
|
446
|
+
function buildThemeVariables(colors) {
|
|
447
|
+
return [
|
|
448
|
+
`--cf-bg-primary: ${colors.background.primary};`,
|
|
449
|
+
`--cf-bg-secondary: ${colors.background.secondary};`,
|
|
450
|
+
`--cf-bg-tertiary: ${colors.background.tertiary};`,
|
|
451
|
+
`--cf-text-primary: ${colors.text.primary};`,
|
|
452
|
+
`--cf-text-secondary: ${colors.text.secondary};`,
|
|
453
|
+
`--cf-text-disabled: ${colors.text.disabled};`,
|
|
454
|
+
`--cf-border-default: ${colors.border.default};`,
|
|
455
|
+
`--cf-border-strong: ${colors.border.strong};`,
|
|
456
|
+
`--cf-accent-blue: ${colors.accent.blue};`,
|
|
457
|
+
`--cf-accent-green: ${colors.accent.green};`,
|
|
458
|
+
`--cf-accent-orange: ${colors.accent.orange};`,
|
|
459
|
+
`--cf-accent-red: ${colors.accent.red};`,
|
|
460
|
+
`--colla-md-selection: color-mix(in srgb, ${colors.accent.blue} 15%, transparent);`
|
|
461
|
+
].join("\n");
|
|
462
|
+
}
|
|
463
|
+
function createThemePlugin(theme = "light") {
|
|
464
|
+
return (ctx) => async () => {
|
|
465
|
+
await ctx.wait(EditorViewReady);
|
|
466
|
+
const root = ctx.get(rootDOMCtx);
|
|
467
|
+
const colors = theme === "dark" ? darkTheme.colors : lightTheme.colors;
|
|
468
|
+
root.classList.add("colla-md", `colla-md--${theme}`);
|
|
469
|
+
root.dataset.theme = theme;
|
|
470
|
+
root.style.cssText += buildThemeVariables(colors);
|
|
471
|
+
injectNotionStyle();
|
|
472
|
+
injectCollabCursorStyle();
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/editor/factory.ts
|
|
477
|
+
function buildPlugins(resolved, content) {
|
|
478
|
+
const highlight = resolved.highlight ? createHighlightPlugin(resolved.highlight === true ? void 0 : resolved.highlight) : null;
|
|
479
|
+
const plugins = [
|
|
480
|
+
createThemePlugin(resolved.theme),
|
|
481
|
+
...highlight?.plugins ?? [],
|
|
482
|
+
...layoutNodes,
|
|
483
|
+
...resolved.plugins ?? [],
|
|
484
|
+
createCollabPlugins(content)
|
|
485
|
+
];
|
|
486
|
+
return { plugins, configureHighlight: highlight?.configure };
|
|
487
|
+
}
|
|
488
|
+
function setupAwarenessChangeListener(provider, onChange) {
|
|
489
|
+
if (!provider || !onChange) return null;
|
|
490
|
+
const handler = () => onChange(provider.getAwarenessUsers());
|
|
491
|
+
provider.awareness.on("change", handler);
|
|
492
|
+
handler();
|
|
493
|
+
return () => provider.awareness.off("change", handler);
|
|
494
|
+
}
|
|
495
|
+
async function createEditor(options) {
|
|
496
|
+
const resolved = resolveEditorOptions(options);
|
|
497
|
+
const provider = resolved.collab ? new CollaFlowProvider(resolved.collab) : null;
|
|
498
|
+
const content = provider ? provider.content : new Y4.Doc().getText("content");
|
|
499
|
+
if (content.length === 0 && resolved.defaultValue) {
|
|
500
|
+
content.insert(0, resolved.defaultValue);
|
|
501
|
+
}
|
|
502
|
+
const { plugins: dynamicPlugins, configureHighlight } = buildPlugins(resolved, content);
|
|
503
|
+
const editor = await Editor.make().config((ctx) => {
|
|
504
|
+
ctx.set(rootCtx, resolved.root);
|
|
505
|
+
ctx.set(defaultValueCtx, resolved.defaultValue);
|
|
506
|
+
configureHighlight?.(ctx);
|
|
507
|
+
if (resolved.onChange) {
|
|
508
|
+
ctx.get(listenerCtx2).markdownUpdated((_, markdown2) => {
|
|
509
|
+
resolved.onChange(unescapeLeadingHash(markdown2));
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}).use(commonmark).use(listener).use(block).use(dynamicPlugins).create();
|
|
513
|
+
if (provider) {
|
|
514
|
+
provider.connect();
|
|
515
|
+
}
|
|
516
|
+
const removeAwarenessChangeListener = setupAwarenessChangeListener(
|
|
517
|
+
provider,
|
|
518
|
+
resolved.onAwarenessChange
|
|
519
|
+
);
|
|
520
|
+
return {
|
|
521
|
+
get editor() {
|
|
522
|
+
return editor;
|
|
523
|
+
},
|
|
524
|
+
get content() {
|
|
525
|
+
return content;
|
|
526
|
+
},
|
|
527
|
+
getView() {
|
|
528
|
+
return editor.action((ctx) => ctx.get(editorViewCtx));
|
|
529
|
+
},
|
|
530
|
+
getMarkdown() {
|
|
531
|
+
return unescapeLeadingHash(editor.action(getMarkdown()));
|
|
532
|
+
},
|
|
533
|
+
async setMarkdown(value) {
|
|
534
|
+
await editor.action(replaceAll2(value));
|
|
535
|
+
},
|
|
536
|
+
getAwarenessUsers() {
|
|
537
|
+
return provider?.getAwarenessUsers() ?? [];
|
|
538
|
+
},
|
|
539
|
+
setLocalSelection(anchor, head) {
|
|
540
|
+
provider?.setLocalSelection(anchor, head);
|
|
541
|
+
},
|
|
542
|
+
getRemoteCursors() {
|
|
543
|
+
return provider?.getRemoteCursors() ?? [];
|
|
544
|
+
},
|
|
545
|
+
async destroy() {
|
|
546
|
+
removeAwarenessChangeListener?.();
|
|
547
|
+
if (provider) {
|
|
548
|
+
provider.destroy();
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
550
|
+
}
|
|
551
|
+
await editor.destroy();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/editor/plugins/cursor-map.ts
|
|
557
|
+
function buildDomTextCounts(doc, markdown2) {
|
|
558
|
+
const counts = new Array(markdown2.length + 1).fill(0);
|
|
559
|
+
let md = 0;
|
|
560
|
+
let count = 0;
|
|
561
|
+
const text = (n) => {
|
|
562
|
+
for (let i = 0; i < n && md < markdown2.length; i++) {
|
|
563
|
+
counts[md] = count;
|
|
564
|
+
md++;
|
|
565
|
+
count++;
|
|
566
|
+
}
|
|
567
|
+
if (md <= markdown2.length) counts[md] = count;
|
|
568
|
+
};
|
|
569
|
+
const syntax = (n) => {
|
|
570
|
+
for (let i = 0; i < n && md < markdown2.length; i++) {
|
|
571
|
+
counts[md] = count;
|
|
572
|
+
md++;
|
|
573
|
+
}
|
|
574
|
+
if (md <= markdown2.length) counts[md] = count;
|
|
575
|
+
};
|
|
576
|
+
const inline = (node) => {
|
|
577
|
+
node.forEach((child) => {
|
|
578
|
+
if (child.isText) {
|
|
579
|
+
const marks = child.marks.map((m) => m.type.name);
|
|
580
|
+
const str = child.text;
|
|
581
|
+
if (marks.includes("inlineCode") || marks.includes("code")) {
|
|
582
|
+
syntax(1);
|
|
583
|
+
text(str.length);
|
|
584
|
+
syntax(1);
|
|
585
|
+
} else {
|
|
586
|
+
if (marks.includes("strong")) syntax(2);
|
|
587
|
+
if (marks.includes("emphasis") || marks.includes("em")) syntax(1);
|
|
588
|
+
text(str.length);
|
|
589
|
+
if (marks.includes("emphasis") || marks.includes("em")) syntax(1);
|
|
590
|
+
if (marks.includes("strong")) syntax(2);
|
|
591
|
+
}
|
|
592
|
+
} else if (child.type.name === "hardbreak" || child.type.name === "hard_break") {
|
|
593
|
+
syntax(1);
|
|
594
|
+
} else {
|
|
595
|
+
text(child.textContent.length);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
const fence = (node) => {
|
|
600
|
+
const lang = node.attrs.language ?? "";
|
|
601
|
+
syntax(3);
|
|
602
|
+
syntax(lang.length);
|
|
603
|
+
syntax(1);
|
|
604
|
+
text(node.textContent.length);
|
|
605
|
+
syntax(1);
|
|
606
|
+
syntax(3);
|
|
607
|
+
};
|
|
608
|
+
const walk = (node) => {
|
|
609
|
+
switch (node.type.name) {
|
|
610
|
+
case "doc":
|
|
611
|
+
node.forEach(walk);
|
|
612
|
+
return;
|
|
613
|
+
case "paragraph":
|
|
614
|
+
inline(node);
|
|
615
|
+
syntax(2);
|
|
616
|
+
return;
|
|
617
|
+
case "heading":
|
|
618
|
+
syntax(node.attrs.level + 1);
|
|
619
|
+
inline(node);
|
|
620
|
+
syntax(2);
|
|
621
|
+
return;
|
|
622
|
+
case "blockquote":
|
|
623
|
+
syntax(2);
|
|
624
|
+
inline(node);
|
|
625
|
+
syntax(2);
|
|
626
|
+
return;
|
|
627
|
+
case "code_block":
|
|
628
|
+
fence(node);
|
|
629
|
+
syntax(2);
|
|
630
|
+
return;
|
|
631
|
+
case "bullet_list":
|
|
632
|
+
node.forEach((li) => {
|
|
633
|
+
syntax(2);
|
|
634
|
+
walk(li);
|
|
635
|
+
});
|
|
636
|
+
syntax(2);
|
|
637
|
+
return;
|
|
638
|
+
case "ordered_list":
|
|
639
|
+
node.forEach((li, i) => {
|
|
640
|
+
syntax(String(i + 1).length + 2);
|
|
641
|
+
walk(li);
|
|
642
|
+
});
|
|
643
|
+
syntax(2);
|
|
644
|
+
return;
|
|
645
|
+
case "list_item":
|
|
646
|
+
node.forEach(walk);
|
|
647
|
+
return;
|
|
648
|
+
default:
|
|
649
|
+
text(node.textContent.length);
|
|
650
|
+
syntax(2);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
walk(doc);
|
|
655
|
+
syntax(markdown2.length - md);
|
|
656
|
+
return counts;
|
|
657
|
+
}
|
|
658
|
+
function markdownOffsetToDomTextOffset(counts, offset) {
|
|
659
|
+
const clamped = Math.max(0, Math.min(offset, counts.length - 1));
|
|
660
|
+
return counts[clamped];
|
|
661
|
+
}
|
|
662
|
+
export {
|
|
663
|
+
CollaFlowProvider,
|
|
664
|
+
YJS_ORIGIN_MILKDOWN,
|
|
665
|
+
YJS_ORIGIN_TEXTAREA,
|
|
666
|
+
buildDomTextCounts,
|
|
667
|
+
createCollabPlugins,
|
|
668
|
+
createEditor,
|
|
669
|
+
decodeOffset,
|
|
670
|
+
decodeSelection,
|
|
671
|
+
diffApply,
|
|
672
|
+
encodeRelativePosition2 as encodeRelativePosition,
|
|
673
|
+
markdownOffsetToDomTextOffset
|
|
674
|
+
};
|
|
675
|
+
//# sourceMappingURL=index.js.map
|