@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/editor/factory.ts","../src/editor/options.ts","../src/collab/provider.ts","../src/collab/cursor.ts","../src/collab/yjs-binding.ts","../src/editor/plugins/awareness.ts","../src/editor/plugins/highlight.ts","../src/editor/plugins/layout.ts","../src/editor/plugins/theme.ts","../src/styles/notion-style.ts","../src/editor/plugins/cursor-map.ts"],"sourcesContent":["export { createEditor } from './editor';\nexport {\n CollaFlowProvider,\n createCollabPlugins,\n diffApply,\n YJS_ORIGIN_MILKDOWN,\n YJS_ORIGIN_TEXTAREA,\n decodeSelection,\n decodeOffset,\n encodeRelativePosition,\n type EncodedSelection,\n type RemoteCursor,\n} from './collab';\nexport {\n buildDomTextCounts,\n markdownOffsetToDomTextOffset,\n} from './editor/plugins/cursor-map';\nexport type {\n AwarenessUserInfo,\n CollabOptions,\n HighlightOptions,\n ThemeConfig,\n CollaMarkdownEditorOptions,\n CollaMarkdownEditor,\n} from './types';\n","import * as Y from 'yjs';\nimport { Editor, defaultValueCtx, editorViewCtx, rootCtx } from '@milkdown/core';\nimport type { Ctx, MilkdownPlugin } from '@milkdown/ctx';\nimport { commonmark } from '@milkdown/preset-commonmark';\nimport { listener, listenerCtx } from '@milkdown/plugin-listener';\nimport { getMarkdown, replaceAll } from '@milkdown/utils';\nimport { block } from '@milkdown/plugin-block';\nimport type { EditorView } from 'prosemirror-view';\n\nimport type { CollaMarkdownEditor, CollaMarkdownEditorOptions } from '../types';\nimport { resolveEditorOptions } from './options';\nimport { CollaFlowProvider, createCollabPlugins, unescapeLeadingHash } from '../collab';\nimport { createHighlightPlugin, layoutNodes, createThemePlugin } from './plugins';\n\n/**\n * 构建编辑器所需的 Milkdown 插件列表。\n *\n * 协同绑定以 Markdown 文本(Y.Text)为唯一真相:源码区与预览区都读写同一份文本,\n * Y.Text 的字符级 CRDT 自动合并多人并发编辑。\n */\nfunction buildPlugins(\n resolved: ReturnType<typeof resolveEditorOptions>,\n content: Y.Text,\n): { plugins: MilkdownPlugin[]; configureHighlight?: (ctx: Ctx) => void } {\n const highlight = resolved.highlight\n ? createHighlightPlugin(resolved.highlight === true ? undefined : resolved.highlight)\n : null;\n\n const plugins: MilkdownPlugin[] = [\n createThemePlugin(resolved.theme),\n ...(highlight?.plugins ?? []),\n ...layoutNodes,\n ...(resolved.plugins ?? []),\n createCollabPlugins(content),\n ];\n\n return { plugins, configureHighlight: highlight?.configure };\n}\n\n/**\n * 注册在线用户列表变化监听。\n */\nfunction setupAwarenessChangeListener(\n provider: CollaFlowProvider | null,\n onChange: CollaMarkdownEditorOptions['onAwarenessChange']\n): (() => void) | null {\n if (!provider || !onChange) return null;\n\n const handler = () => onChange(provider.getAwarenessUsers());\n provider.awareness.on('change', handler);\n handler(); // 初始触发一次\n\n return () => provider.awareness.off('change', handler);\n}\n\n/**\n * 创建 CollaMarkdown 编辑器实例。\n *\n * Phase 1~4 实现能力:\n * - 基于 Milkdown 渲染 Markdown\n * - 接入 listener 插件,支持 onChange 回调\n * - 默认启用 commonmark、prism 代码高亮、block 菜单\n * - 以 Markdown 文本(Y.Text)为唯一真相的可选协同:源码区与预览区都读写同一份文本\n * - 自定义布局节点(Callout / Columns / Column / Card)\n * - CollaFlow 主题 CSS 变量\n */\nexport async function createEditor(\n options: CollaMarkdownEditorOptions\n): Promise<CollaMarkdownEditor> {\n const resolved = resolveEditorOptions(options);\n\n const provider = resolved.collab ? new CollaFlowProvider(resolved.collab) : null;\n // 协同取共享文档的 content;本地模式用独立 Y.Doc 承载同一份文本,行为一致。\n const content = provider ? provider.content : new Y.Doc().getText('content');\n if (content.length === 0 && resolved.defaultValue) {\n content.insert(0, resolved.defaultValue);\n }\n\n const { plugins: dynamicPlugins, configureHighlight } = buildPlugins(resolved, content);\n\n const editor = await Editor.make()\n .config((ctx) => {\n ctx.set(rootCtx, resolved.root);\n ctx.set(defaultValueCtx, resolved.defaultValue);\n\n configureHighlight?.(ctx);\n\n if (resolved.onChange) {\n ctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n resolved.onChange!(unescapeLeadingHash(markdown));\n });\n }\n })\n .use(commonmark)\n .use(listener)\n .use(block)\n .use(dynamicPlugins)\n .create();\n\n if (provider) {\n provider.connect();\n }\n\n const removeAwarenessChangeListener = setupAwarenessChangeListener(\n provider,\n resolved.onAwarenessChange\n );\n\n return {\n get editor() {\n return editor;\n },\n get content() {\n return content;\n },\n getView(): EditorView {\n return editor.action((ctx) => ctx.get(editorViewCtx)) as unknown as EditorView;\n },\n getMarkdown() {\n return unescapeLeadingHash(editor.action(getMarkdown()));\n },\n async setMarkdown(value: string) {\n await editor.action(replaceAll(value));\n },\n getAwarenessUsers() {\n return provider?.getAwarenessUsers() ?? [];\n },\n setLocalSelection(anchor: number, head: number): void {\n provider?.setLocalSelection(anchor, head);\n },\n getRemoteCursors() {\n return provider?.getRemoteCursors() ?? [];\n },\n async destroy() {\n removeAwarenessChangeListener?.();\n if (provider) {\n provider.destroy();\n // 给 y-prosemirror 预留一个 tick 清理 pending 的 setMeta timeout\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n await editor.destroy();\n },\n };\n}\n","import type { CollaMarkdownEditorOptions } from '../types';\n\n/**\n * 编辑器默认配置。\n */\nexport const DEFAULT_EDITOR_OPTIONS: Required<\n Pick<CollaMarkdownEditorOptions, 'defaultValue' | 'highlight' | 'theme'>\n> = {\n defaultValue: '',\n highlight: true,\n theme: 'light',\n};\n\n/**\n * 合并用户配置与默认配置。\n */\nexport function resolveEditorOptions(\n options: CollaMarkdownEditorOptions\n): Required<Omit<CollaMarkdownEditorOptions, 'collab' | 'plugins' | 'onChange' | 'onAwarenessChange'>> &\n Pick<CollaMarkdownEditorOptions, 'collab' | 'plugins' | 'onChange' | 'onAwarenessChange'> {\n return {\n root: options.root,\n defaultValue: options.defaultValue ?? DEFAULT_EDITOR_OPTIONS.defaultValue,\n highlight: options.highlight ?? DEFAULT_EDITOR_OPTIONS.highlight,\n theme: options.theme ?? DEFAULT_EDITOR_OPTIONS.theme,\n collab: options.collab,\n plugins: options.plugins,\n onChange: options.onChange,\n onAwarenessChange: options.onAwarenessChange,\n };\n}\n","import * as Y from 'yjs';\nimport * as awarenessProtocol from 'y-protocols/awareness';\nimport { HocuspocusProvider } from '@hocuspocus/provider';\n\nimport type { CollabOptions, AwarenessUserInfo, AwarenessUserState } from '../types';\nimport {\n decodeSelection,\n encodeRelativePosition,\n type EncodedSelection,\n type RemoteCursor,\n} from './cursor';\n\n/**\n * CollaFlow 协同 Provider。\n *\n * 职责:\n * - 维护 Y.Doc 与 Awareness 实例\n * - 通过 @hocuspocus/provider 与 CollaFlow Core 的 Hocuspocus 服务通信\n * - 暴露在线用户列表\n *\n * 传输层(Yjs sync + awareness 协议)完全交给 Hocuspocus,\n * 本类只做生命周期与本地状态的收口。\n */\nexport class CollaFlowProvider {\n readonly doc: Y.Doc;\n readonly awareness: awarenessProtocol.Awareness;\n\n private readonly provider: HocuspocusProvider;\n private connected = false;\n\n constructor(options: CollabOptions) {\n this.doc = new Y.Doc();\n this.awareness = new awarenessProtocol.Awareness(this.doc);\n\n if (options.user) {\n this.awareness.setLocalState({ user: options.user });\n }\n\n this.provider = new HocuspocusProvider({\n url: options.serverUrl,\n name: options.roomId,\n document: this.doc,\n awareness: this.awareness,\n onStatus: ({ status }) => {\n this.connected = status === 'connected';\n },\n });\n }\n\n /**\n * 当前是否已与协同服务建立连接。\n */\n get isConnected(): boolean {\n return this.connected;\n }\n\n /**\n * 协同内容源:以 Markdown 文本为唯一真相的 Y.Text。\n *\n * 源码区与预览区都读写这一份文本;协同(Hocuspocus)只负责同步它,\n * 因此多人编辑按字符级 CRDT 合并,不会再出现「整体替换」式的覆盖。\n */\n get content(): Y.Text {\n return this.doc.getText('content');\n }\n\n /**\n * 获取当前在线用户列表。\n */\n getAwarenessUsers(): AwarenessUserState[] {\n const users: AwarenessUserState[] = [];\n\n this.awareness.getStates().forEach((state, clientId) => {\n const user = state?.user as AwarenessUserInfo | undefined;\n if (user?.name && user?.color) {\n users.push({ clientId, user });\n }\n });\n\n return users;\n }\n\n /**\n * 建立连接。\n */\n connect(): void {\n this.provider.connect();\n }\n\n /**\n * 上报本地选区(以 Y.Text 字符偏移表示)。\n *\n * 会被编码为相对位置存入 awareness,随并发编辑自愈,避免光标错位。\n */\n setLocalSelection(anchor: number, head: number): void {\n const selection: EncodedSelection = {\n anchor: encodeRelativePosition(this.content, anchor),\n head: encodeRelativePosition(this.content, head),\n };\n this.awareness.setLocalStateField('selection', selection);\n }\n\n /**\n * 读取远端用户的光标列表(已解码为 Y.Text 绝对偏移)。\n */\n getRemoteCursors(): RemoteCursor[] {\n const cursors: RemoteCursor[] = [];\n\n this.awareness.getStates().forEach((state, clientId) => {\n if (clientId === this.doc.clientID) return;\n const user = state?.user as AwarenessUserInfo | undefined;\n const selection = state?.selection as EncodedSelection | undefined;\n if (!user?.name || !user?.color || !selection) return;\n\n const offsets = decodeSelection(this.content, selection);\n if (!offsets) return;\n\n cursors.push({ clientId, user, anchor: offsets.anchor, head: offsets.head });\n });\n\n return cursors;\n }\n\n /**\n * 断开连接(保留本地 doc 内容,可再次 connect)。\n */\n disconnect(): void {\n this.provider.disconnect();\n }\n\n /**\n * 销毁 Provider,清理连接与 Yjs 资源。\n */\n destroy(): void {\n this.provider.destroy();\n this.awareness.destroy();\n this.doc.destroy();\n }\n}\n","import * as Y from 'yjs';\n\nimport type { AwarenessUserInfo } from '../types';\n\n/**\n * awareness 中携带的远端选区(已序列化为字符串,便于 JSON 传输)。\n */\nexport interface EncodedSelection {\n /** 选区起点(Y.Text 相对位置,base64) */\n anchor: string;\n /** 选区终点(Y.Text 相对位置,base64) */\n head: string;\n}\n\n/**\n * 解析后的远端光标(已换算为 Y.Text 绝对偏移)。\n */\nexport interface RemoteCursor {\n /** Yjs 客户端 ID */\n clientId: number;\n /** 用户信息(名称 / 颜色) */\n user: AwarenessUserInfo;\n /** 选区起点在 Y.Text 中的字符偏移 */\n anchor: number;\n /** 选区终点在 Y.Text 中的字符偏移 */\n head: number;\n}\n\nfunction toBase64(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i] as number);\n return btoa(binary);\n}\n\nfunction fromBase64(text: string): Uint8Array {\n const binary = atob(text);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n/**\n * 将 Y.Text 的字符偏移编码为可传输的相对位置。\n *\n * 相对位置记录的是「哪一项」,而非「第几个字符」,因此并发插入导致下标漂移时仍能自愈,\n * 这是协同光标不会错位的关键。\n */\nexport function encodeRelativePosition(yText: Y.Text, index: number): string {\n const rel = Y.createRelativePositionFromTypeIndex(yText, index);\n return toBase64(Y.encodeRelativePosition(rel));\n}\n\n/**\n * 将编码后的相对位置解码为当前文档下的绝对偏移。\n * 若该位置已随编辑被移除(超出文档范围),返回 null。\n */\nexport function decodeOffset(yText: Y.Text, encoded: string): number | null {\n try {\n const rel = Y.decodeRelativePosition(fromBase64(encoded));\n const doc = yText.doc;\n /* c8 ignore next */\n if (!doc) return null;\n const abs = Y.createAbsolutePositionFromRelativePosition(rel, doc);\n /* c8 ignore next */\n if (!abs || abs.type !== yText) return null;\n return abs.index;\n } catch {\n return null;\n }\n}\n\n/**\n * 解码远端选区为 Y.Text 绝对偏移。\n */\nexport function decodeSelection(\n yText: Y.Text,\n selection: EncodedSelection\n): { anchor: number; head: number } | null {\n const anchor = decodeOffset(yText, selection.anchor);\n const head = decodeOffset(yText, selection.head);\n if (anchor === null || head === null) return null;\n return { anchor, head };\n}\n","import * as Y from 'yjs';\nimport type { MilkdownPlugin } from '@milkdown/ctx';\nimport { InitReady, editorCtx } from '@milkdown/core';\nimport { listenerCtx } from '@milkdown/plugin-listener';\nimport { replaceAll } from '@milkdown/utils';\n\n/** Milkdown 编辑回写 Y.Text 时使用的 transaction origin。 */\nexport const YJS_ORIGIN_MILKDOWN = 'colla-md:milkdown';\n/** 源码区(textarea)编辑写入 Y.Text 时使用的 transaction origin。 */\nexport const YJS_ORIGIN_TEXTAREA = 'colla-md:textarea';\n\n/**\n * 计算最小差异并将 newText 应用到 yText(字符级增删)。\n *\n * 不采用整体「删除全部 + 重新插入」,否则会与并发的远端编辑冲突(整体替换会清掉他人刚插入的内容)。\n * 通过 transaction origin 标记来源,便于观察者区分「自己写的」与「远端 / 其他端写的」。\n */\nexport function diffApply(yText: Y.Text, newText: string, origin: unknown): void {\n const oldText = yText.toString();\n if (oldText === newText) return;\n\n // 最长公共前缀\n let start = 0;\n const minLen = Math.min(oldText.length, newText.length);\n while (start < minLen && oldText[start] === newText[start]) start++;\n\n // 最长公共后缀\n let endOld = oldText.length;\n let endNew = newText.length;\n while (endOld > start && endNew > start && oldText[endOld - 1] === newText[endNew - 1]) {\n endOld--;\n endNew--;\n }\n\n const deleteLength = endOld - start;\n const insertText = newText.slice(start, endNew);\n\n const ydoc = yText.doc;\n if (!ydoc) return;\n ydoc.transact(() => {\n if (deleteLength > 0) yText.delete(start, deleteLength);\n if (insertText) yText.insert(start, insertText);\n }, origin);\n}\n\n/**\n * 创建「Y.Text ↔ Milkdown」双向绑定插件。\n *\n * 设计前提:**Markdown 文本(Y.Text)是唯一真相**。\n * - Y.Text 变化(远端协同 / 源码区编辑)→ 重新渲染 Milkdown 预览;\n * - Milkdown 编辑 → 序列化 Markdown 写回 Y.Text(哪怕只改了预览区,也优先落到源码)。\n *\n * 借助 transaction origin 防止回环:Milkdown 自身写回的内容(origin = MILKDOWN)不会再次被应用回 Milkdown。\n */\n/**\n * 去掉序列化时行首 `#` 被加上的转义反斜杠。\n *\n * remark-stringify 出于安全会把「行首的 #」写成 `\\#`(否则会被当成标题)。\n * 但需求是「# 就是 #」:行首的 # 作为字面量,标题仍由「# 」(带空格)这种合法语法产生。\n * 这里只处理行首的 `\\#`,不动 `>` / `*` 等其它转义,影响面最小。\n */\nexport function unescapeLeadingHash(markdown: string): string {\n return markdown.replace(/^\\\\#/gm, '#');\n}\n\nexport function createCollabPlugins(content: Y.Text): MilkdownPlugin {\n return (ctx) => async () => {\n await ctx.wait(InitReady);\n const editor = ctx.get(editorCtx);\n\n // Y.Text -> Milkdown\n content.observe((_, transaction) => {\n if (transaction.origin === YJS_ORIGIN_MILKDOWN) return;\n void editor.action(replaceAll(content.toString()));\n });\n\n // Milkdown -> Y.Text\n ctx.get(listenerCtx).markdownUpdated((_, markdown) => {\n diffApply(content, unescapeLeadingHash(markdown), YJS_ORIGIN_MILKDOWN);\n });\n };\n}\n","import { yCursorPlugin } from 'y-prosemirror';\nimport type { Awareness } from 'y-protocols/awareness';\n\nimport type { AwarenessUserInfo } from '../../types';\n\n/**\n * 自定义远端用户光标渲染。\n */\nexport function collaCursorBuilder(user: AwarenessUserInfo, _clientId: number): HTMLElement {\n const cursor = document.createElement('span');\n cursor.className = 'colla-cursor';\n cursor.style.borderLeft = `2px solid ${user.color}`;\n cursor.style.marginLeft = '-1px';\n cursor.style.height = '1.2em';\n cursor.style.display = 'inline-block';\n cursor.style.position = 'relative';\n\n const label = document.createElement('span');\n label.className = 'colla-cursor-label';\n label.textContent = user.name;\n label.style.backgroundColor = user.color;\n label.style.color = '#fff';\n label.style.position = 'absolute';\n label.style.top = '-1.4em';\n label.style.left = '-2px';\n label.style.fontSize = '10px';\n label.style.padding = '2px 4px';\n label.style.borderRadius = '2px';\n label.style.whiteSpace = 'nowrap';\n\n cursor.appendChild(label);\n return cursor;\n}\n\n/**\n * 自定义远端用户选区高亮渲染。\n */\nexport function collaSelectionBuilder(user: AwarenessUserInfo) {\n return {\n class: 'colla-selection',\n style: `background-color: ${user.color}33;`,\n };\n}\n\n/**\n * 创建 Awareness 光标/选区插件。\n */\nexport function createAwarenessPlugin(awareness: Awareness): unknown {\n return yCursorPlugin(awareness, {\n cursorBuilder: collaCursorBuilder as (user: unknown, clientId: number) => HTMLElement,\n selectionBuilder: collaSelectionBuilder as (user: unknown) => {\n class: string;\n style: string;\n },\n });\n}\n","import { refractor } from 'refractor';\nimport type { Syntax } from 'refractor/core';\nimport { prism, prismConfig } from '@milkdown/plugin-prism';\nimport type { MilkdownPlugin, Ctx } from '@milkdown/ctx';\n\n// refractor 的 exports 为 `./* -> ./lang/*.js`,所以语言子路径不带 lang/ 前缀\nimport bash from 'refractor/bash';\nimport css from 'refractor/css';\nimport javascript from 'refractor/javascript';\nimport json from 'refractor/json';\nimport jsx from 'refractor/jsx';\nimport markdown from 'refractor/markdown';\n// HTML 在 refractor 里对应 markup(别名 html)\nimport markup from 'refractor/markup';\nimport python from 'refractor/python';\nimport tsx from 'refractor/tsx';\nimport typescript from 'refractor/typescript';\n\nimport type { HighlightOptions } from '../../types';\n\n/**\n * 默认注册的代码高亮语法。\n *\n * 顺序有意义:typescript / tsx / jsx 依赖 javascript,markdown 依赖 markup。\n */\nexport const DEFAULT_SYNTAXES: Syntax[] = [\n javascript,\n typescript,\n tsx,\n jsx,\n markup,\n css,\n markdown,\n json,\n bash,\n python,\n];\n\n/**\n * 注册 refractor 高亮语法。\n */\nexport function configureHighlightLanguages(syntaxes: Syntax[]): void {\n for (const syntax of syntaxes) {\n refractor.register(syntax);\n }\n}\n\nexport interface HighlightPluginResult {\n plugins: MilkdownPlugin[];\n configure?: (ctx: Ctx) => void;\n}\n\n/**\n * 创建代码高亮插件。\n */\nexport function createHighlightPlugin(options?: HighlightOptions): HighlightPluginResult {\n const isShiki = options?.type === 'shiki';\n\n if (isShiki) {\n // TODO: Phase 4+ 可切换为 Shiki\n return { plugins: prism };\n }\n\n configureHighlightLanguages(DEFAULT_SYNTAXES);\n\n return {\n plugins: prism,\n configure: (ctx) => {\n ctx.set(prismConfig.key, { configureRefractor: (r) => r });\n },\n };\n}\n\nexport { refractor, prism, prismConfig };\n","import { $node } from '@milkdown/utils';\nimport type { MilkdownPlugin } from '@milkdown/ctx';\nimport type { NodeSchema } from '@milkdown/transformer';\n\n/**\n * 空 Markdown 解析/序列化配置。\n *\n * 当前布局节点主要支持编辑器内渲染与 HTML 输出,\n * Markdown 双向转换将在后续阶段通过 remark 插件完善。\n */\n// 布局节点暂不支持 Markdown 双向转换:match 恒为 false,runner 永不执行。\n/* v8 ignore start */\nconst emptyMarkdownSpec: Pick<NodeSchema, 'parseMarkdown' | 'toMarkdown'> = {\n parseMarkdown: {\n match: () => false,\n runner: () => {},\n },\n toMarkdown: {\n match: () => false,\n runner: () => {},\n },\n};\n/* v8 ignore stop */\n\n/**\n * Callout 提示框节点。\n *\n * HTML 输出:<div class=\"callout callout--info\" data-type=\"info\">...</div>\n */\nexport const calloutNode = $node('callout', () => ({\n group: 'block',\n content: 'block+',\n attrs: {\n type: { default: 'info' },\n },\n parseDOM: [\n {\n tag: 'div.callout',\n getAttrs: (dom) => {\n const element = dom as HTMLElement;\n return { type: element.dataset.type ?? 'info' };\n },\n },\n ],\n toDOM: (node) => [\n 'div',\n { class: `callout callout--${node.attrs.type}`, 'data-type': node.attrs.type },\n 0,\n ],\n ...emptyMarkdownSpec,\n}));\n\n/**\n * 分栏容器节点。\n */\nexport const columnsNode = $node('columns', () => ({\n group: 'block',\n content: 'column+',\n attrs: {\n columns: { default: 2 },\n },\n parseDOM: [\n {\n tag: 'div.columns',\n getAttrs: (dom) => {\n const element = dom as HTMLElement;\n return { columns: Number(element.dataset.columns) || 2 };\n },\n },\n ],\n toDOM: (node) => ['div', { class: 'columns', 'data-columns': node.attrs.columns }, 0],\n ...emptyMarkdownSpec,\n}));\n\n/**\n * 单栏节点。\n */\nexport const columnNode = $node('column', () => ({\n group: 'block',\n content: 'block+',\n parseDOM: [{ tag: 'div.column' }],\n toDOM: () => ['div', { class: 'column' }, 0],\n ...emptyMarkdownSpec,\n}));\n\n/**\n * 卡片容器节点。\n */\nexport const cardNode = $node('card', () => ({\n group: 'block',\n content: 'block+',\n attrs: {\n title: { default: '' },\n },\n parseDOM: [\n {\n tag: 'div.card',\n getAttrs: (dom) => {\n const element = dom as HTMLElement;\n return { title: element.dataset.title ?? '' };\n },\n },\n ],\n toDOM: (node) => ['div', { class: 'card', 'data-title': node.attrs.title }, 0],\n ...emptyMarkdownSpec,\n}));\n\n/**\n * 布局节点插件集合。\n */\nexport const layoutNodes: MilkdownPlugin[] = [\n calloutNode,\n columnsNode,\n columnNode,\n cardNode,\n];\n","import { EditorViewReady, rootDOMCtx } from '@milkdown/core';\nimport type { MilkdownPlugin } from '@milkdown/ctx';\nimport { lightTheme, darkTheme } from '@collaflow/design';\n\nimport type { ThemeConfig } from '../../types';\nimport { injectNotionStyle, injectCollabCursorStyle } from '../../styles/notion-style';\n\ntype ThemeColors = typeof lightTheme.colors;\n\n/**\n * 把 @collaflow/design 的主题色展开成 CSS 变量。\n *\n * 变量名与 design 包的 `css/variables.css`(`--cf-*`)保持一致,\n * 这样即使接入方没有引入 design 的 CSS 文件,编辑器也能拿到完整配色。\n */\nexport function buildThemeVariables(colors: ThemeColors): string {\n return [\n `--cf-bg-primary: ${colors.background.primary};`,\n `--cf-bg-secondary: ${colors.background.secondary};`,\n `--cf-bg-tertiary: ${colors.background.tertiary};`,\n `--cf-text-primary: ${colors.text.primary};`,\n `--cf-text-secondary: ${colors.text.secondary};`,\n `--cf-text-disabled: ${colors.text.disabled};`,\n `--cf-border-default: ${colors.border.default};`,\n `--cf-border-strong: ${colors.border.strong};`,\n `--cf-accent-blue: ${colors.accent.blue};`,\n `--cf-accent-green: ${colors.accent.green};`,\n `--cf-accent-orange: ${colors.accent.orange};`,\n `--cf-accent-red: ${colors.accent.red};`,\n `--colla-md-selection: color-mix(in srgb, ${colors.accent.blue} 15%, transparent);`,\n ].join('\\n');\n}\n\n/**\n * 创建主题插件:为编辑器根节点注入主题类名与 CSS 变量。\n */\nexport function createThemePlugin(theme: ThemeConfig = 'light'): MilkdownPlugin {\n return (ctx) => async () => {\n await ctx.wait(EditorViewReady);\n\n const root = ctx.get(rootDOMCtx);\n const colors = theme === 'dark' ? darkTheme.colors : lightTheme.colors;\n\n root.classList.add('colla-md', `colla-md--${theme}`);\n root.dataset.theme = theme;\n root.style.cssText += buildThemeVariables(colors);\n\n injectNotionStyle();\n injectCollabCursorStyle();\n };\n}\n","/**\n * Notion 风格的编辑器内容样式。\n *\n * 作用于 `.colla-md`(Milkdown 根节点),颜色全部引用 design 的 `--cf-*` 变量,\n * 因此随 light / dark 主题自动切换。字体采用 Notion 的系统字体栈。\n *\n * 该样式在 `createThemePlugin` 初始化时由 `injectNotionStyle()` 注入一次,\n * 编辑器为纯客户端组件,无需 SSR 守卫。\n */\n\n/** Notion 正文使用的系统字体栈 */\nexport const NOTION_FONT_FAMILY =\n 'ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, \"Apple Color Emoji\", Arial, sans-serif';\n\n/** 等宽字体(行内代码 / 代码块) */\nexport const NOTION_MONO_FAMILY =\n 'SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace';\n\n/** Notion 风格编辑器样式表(已按 `.colla-md` 作用域限定) */\nexport const NOTION_EDITOR_CSS = `\n.colla-md {\n font-family: ${NOTION_FONT_FAMILY};\n font-size: 16px;\n line-height: 1.5;\n color: var(--cf-text-primary);\n font-weight: 400;\n word-break: break-word;\n -webkit-font-smoothing: antialiased;\n}\n.colla-md h1 { font-size: 1.875em; font-weight: 700; line-height: 1.3; margin: 1.4em 0 0.2em; }\n.colla-md h2 { font-size: 1.5em; font-weight: 600; line-height: 1.3; margin: 1.2em 0 0.2em; }\n.colla-md h3 { font-size: 1.25em; font-weight: 600; line-height: 1.3; margin: 1em 0 0.2em; }\n.colla-md h4 { font-size: 1em; font-weight: 600; line-height: 1.3; margin: 0.8em 0 0.2em; }\n.colla-md h5, .colla-md h6 { font-size: 0.875em; font-weight: 600; line-height: 1.3; margin: 0.8em 0 0.2em; }\n.colla-md p { margin: 0; padding: 3px 2px; line-height: 1.5; }\n.colla-md ul, .colla-md ol { margin: 0; padding-left: 1.6em; padding-top: 2px; padding-bottom: 2px; }\n.colla-md li { margin: 2px 0; }\n.colla-md li > p { padding: 0; }\n.colla-md :not(pre) > code {\n font-family: ${NOTION_MONO_FAMILY};\n background: rgba(135, 131, 120, 0.15);\n color: var(--cf-text-primary);\n border-radius: 4px;\n padding: 0.2em 0.4em;\n font-size: 0.85em;\n}\n.colla-md pre {\n font-family: ${NOTION_MONO_FAMILY};\n background: var(--cf-bg-tertiary);\n color: var(--cf-text-primary);\n border-radius: 4px;\n padding: 1em 1.2em;\n margin: 4px 0;\n overflow: auto;\n font-size: 0.85em;\n line-height: 1.45;\n}\n.colla-md pre code { font-family: inherit; background: none; padding: 0; }\n.colla-md blockquote {\n border-left: 3px solid var(--cf-border-strong);\n padding-left: 12px;\n margin: 4px 0;\n color: var(--cf-text-secondary);\n}\n.colla-md a { color: var(--cf-accent-blue); text-decoration: none; }\n.colla-md a:hover { text-decoration: underline; }\n.colla-md strong { font-weight: 600; }\n.colla-md hr { border: none; border-top: 1px solid var(--cf-border-default); margin: 6px 0; }\n.colla-md img { max-width: 100%; border-radius: 4px; }\n`;\n\n/**\n * 协同远端光标的叠层样式(不限定在 .colla-md 内,因为光标层是预览/源码区的兄弟节点)。\n * 具体颜色由调用方按用户色通过 inline style 设置。\n */\nexport const COLLAB_CURSOR_CSS = `\n.colla-rc-cursor {\n position: absolute;\n width: 2px;\n pointer-events: none;\n z-index: 20;\n}\n.colla-rc-label {\n position: absolute;\n top: -1.25em;\n left: -1px;\n font-size: 11px;\n line-height: 1;\n padding: 2px 5px;\n border-radius: 3px 3px 3px 0;\n color: #fff;\n white-space: nowrap;\n font-family: ${NOTION_FONT_FAMILY};\n pointer-events: none;\n user-select: none;\n z-index: 21;\n}\n`;\n\nconst NOTION_STYLE_ID = 'colla-md-notion-style';\nconst COLLAB_CURSOR_STYLE_ID = 'colla-md-collab-cursor';\n\n/**\n * 向文档注入一次 Notion 风格样式表(幂等:已存在则跳过)。\n */\nexport function injectNotionStyle(): void {\n if (document.getElementById(NOTION_STYLE_ID)) return;\n\n const style = document.createElement('style');\n style.id = NOTION_STYLE_ID;\n style.textContent = NOTION_EDITOR_CSS;\n document.head.appendChild(style);\n}\n\n/**\n * 向文档注入一次协同光标样式表(幂等)。\n */\nexport function injectCollabCursorStyle(): void {\n if (document.getElementById(COLLAB_CURSOR_STYLE_ID)) return;\n\n const style = document.createElement('style');\n style.id = COLLAB_CURSOR_STYLE_ID;\n style.textContent = COLLAB_CURSOR_CSS;\n document.head.appendChild(style);\n}\n","import type { Node as PMNode } from 'prosemirror-model';\n\n/**\n * 预览区(Milkdown / ProseMirror)是富文本,而协同真相是 Y.Text(Markdown 纯文本)。\n * 远端光标要落进预览区,需要知道「Markdown 第 o 个字符」在渲染后的 DOM 里对应第几个「真实文本字符」。\n *\n * 本函数同步遍历 ProseMirror 文档与 Markdown 字符串,统计每个 Markdown 下标之前有多少字符是\n * 「真实 DOM 文本」(即去掉 `#`/`*`/反引号/换行等语法字符后的内容)。返回的数组长度为\n * markdown.length + 1,counts[i] = markdown[0..i) 中的真实文本字符数。\n *\n * 配合在预览容器里按「真实文本字符偏移」定位 DOM 节点,即可用 getBoundingClientRect 算出远端光标坐标,\n * 无需把 Markdown 偏移映射到 ProseMirror 坐标(那需要解析序列化格式,更脆弱)。\n *\n * 注:覆盖 commonmark 常见节点;自定义布局节点(Callout/Columns 等)按纯文本近似处理,光标可能略有偏差。\n */\nexport function buildDomTextCounts(doc: PMNode, markdown: string): number[] {\n const counts = new Array<number>(markdown.length + 1).fill(0);\n let md = 0;\n let count = 0;\n\n const text = (n: number) => {\n for (let i = 0; i < n && md < markdown.length; i++) {\n counts[md] = count;\n md++;\n count++;\n }\n if (md <= markdown.length) counts[md] = count;\n };\n\n const syntax = (n: number) => {\n for (let i = 0; i < n && md < markdown.length; i++) {\n counts[md] = count;\n md++;\n }\n if (md <= markdown.length) counts[md] = count;\n };\n\n const inline = (node: PMNode) => {\n node.forEach((child) => {\n if (child.isText) {\n const marks = child.marks.map((m) => m.type.name);\n const str = child.text as string;\n // Milkdown 的 mark 名为 inlineCode / emphasis(兼容通用名 code / em)\n if (marks.includes('inlineCode') || marks.includes('code')) {\n syntax(1);\n text(str.length);\n syntax(1);\n } else {\n if (marks.includes('strong')) syntax(2);\n if (marks.includes('emphasis') || marks.includes('em')) syntax(1);\n text(str.length);\n if (marks.includes('emphasis') || marks.includes('em')) syntax(1);\n if (marks.includes('strong')) syntax(2);\n }\n } else if (child.type.name === 'hardbreak' || child.type.name === 'hard_break') {\n syntax(1);\n } else {\n text(child.textContent.length);\n }\n });\n };\n\n const fence = (node: PMNode) => {\n const lang = (node.attrs.language as string) ?? '';\n syntax(3);\n syntax(lang.length);\n syntax(1);\n text(node.textContent.length);\n syntax(1);\n syntax(3);\n };\n\n const walk = (node: PMNode) => {\n switch (node.type.name) {\n case 'doc':\n node.forEach(walk);\n return;\n case 'paragraph':\n inline(node);\n syntax(2);\n return;\n case 'heading':\n syntax((node.attrs.level as number) + 1);\n inline(node);\n syntax(2);\n return;\n case 'blockquote':\n syntax(2);\n inline(node);\n syntax(2);\n return;\n case 'code_block':\n fence(node);\n syntax(2);\n return;\n case 'bullet_list':\n node.forEach((li) => {\n syntax(2);\n walk(li);\n });\n syntax(2);\n return;\n case 'ordered_list':\n node.forEach((li, i) => {\n syntax(String(i + 1).length + 2);\n walk(li);\n });\n syntax(2);\n return;\n case 'list_item':\n node.forEach(walk);\n return;\n default:\n // 自定义 / 未知节点:按纯文本近似,并补一个块分隔\n text(node.textContent.length);\n syntax(2);\n return;\n }\n };\n\n walk(doc);\n\n // 收敛:剩余未消费的 markdown 当作语法字符\n syntax(markdown.length - md);\n\n return counts;\n}\n\n/**\n * 将 Y.Text 偏移转换为「真实 DOM 文本偏移」。\n */\nexport function markdownOffsetToDomTextOffset(\n counts: number[],\n offset: number\n): number {\n const clamped = Math.max(0, Math.min(offset, counts.length - 1));\n return counts[clamped] as number;\n}\n"],"mappings":"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","names":["encodeRelativePosition","Y","import_core","import_plugin_listener","import_utils","Y","encodeRelativePosition","encodeRelativePosition","Y","markdown","javascript","typescript","tsx","jsx","markup","css","markdown","json","bash","python","import_utils","import_core","markdown","markdown"]}
@@ -0,0 +1,238 @@
1
+ import { Editor } from '@milkdown/core';
2
+ import { MilkdownPlugin } from '@milkdown/ctx';
3
+ import * as Y from 'yjs';
4
+ import { EditorView } from 'prosemirror-view';
5
+ import * as awarenessProtocol from 'y-protocols/awareness';
6
+ import { Node } from 'prosemirror-model';
7
+
8
+ /**
9
+ * awareness 中携带的远端选区(已序列化为字符串,便于 JSON 传输)。
10
+ */
11
+ interface EncodedSelection {
12
+ /** 选区起点(Y.Text 相对位置,base64) */
13
+ anchor: string;
14
+ /** 选区终点(Y.Text 相对位置,base64) */
15
+ head: string;
16
+ }
17
+ /**
18
+ * 解析后的远端光标(已换算为 Y.Text 绝对偏移)。
19
+ */
20
+ interface RemoteCursor {
21
+ /** Yjs 客户端 ID */
22
+ clientId: number;
23
+ /** 用户信息(名称 / 颜色) */
24
+ user: AwarenessUserInfo;
25
+ /** 选区起点在 Y.Text 中的字符偏移 */
26
+ anchor: number;
27
+ /** 选区终点在 Y.Text 中的字符偏移 */
28
+ head: number;
29
+ }
30
+ /**
31
+ * 将 Y.Text 的字符偏移编码为可传输的相对位置。
32
+ *
33
+ * 相对位置记录的是「哪一项」,而非「第几个字符」,因此并发插入导致下标漂移时仍能自愈,
34
+ * 这是协同光标不会错位的关键。
35
+ */
36
+ declare function encodeRelativePosition(yText: Y.Text, index: number): string;
37
+ /**
38
+ * 将编码后的相对位置解码为当前文档下的绝对偏移。
39
+ * 若该位置已随编辑被移除(超出文档范围),返回 null。
40
+ */
41
+ declare function decodeOffset(yText: Y.Text, encoded: string): number | null;
42
+ /**
43
+ * 解码远端选区为 Y.Text 绝对偏移。
44
+ */
45
+ declare function decodeSelection(yText: Y.Text, selection: EncodedSelection): {
46
+ anchor: number;
47
+ head: number;
48
+ } | null;
49
+
50
+ /**
51
+ * 协同用户信息。
52
+ */
53
+ interface AwarenessUserInfo {
54
+ /** 显示名称 */
55
+ name: string;
56
+ /** 用户颜色(HEX 格式) */
57
+ color: string;
58
+ /** 头像 URL(可选) */
59
+ avatar?: string;
60
+ }
61
+ /**
62
+ * 在线用户状态。
63
+ */
64
+ interface AwarenessUserState {
65
+ /** Yjs 客户端 ID */
66
+ clientId: number;
67
+ /** 用户信息 */
68
+ user: AwarenessUserInfo;
69
+ }
70
+ /**
71
+ * 协同配置。
72
+ */
73
+ interface CollabOptions {
74
+ /** 房间/文档 ID(对应 Hocuspocus 文档名) */
75
+ roomId: string;
76
+ /** CollaFlow Core(Hocuspocus 协同服务)的 WebSocket 地址,如 ws://localhost:1234 */
77
+ serverUrl: string;
78
+ /** 用户 Awareness 信息(用于光标/在线列表展示) */
79
+ user?: AwarenessUserInfo;
80
+ }
81
+ /**
82
+ * 代码高亮配置。
83
+ */
84
+ interface HighlightOptions {
85
+ /** 高亮器类型 */
86
+ type: 'prism' | 'shiki';
87
+ }
88
+ /**
89
+ * 主题配置。
90
+ */
91
+ type ThemeConfig = 'light' | 'dark';
92
+ /**
93
+ * CollaMarkdown 编辑器配置。
94
+ */
95
+ interface CollaMarkdownEditorOptions {
96
+ /** 挂载节点 */
97
+ root: HTMLElement;
98
+ /** 初始 Markdown 内容 */
99
+ defaultValue?: string;
100
+ /** 是否启用协同 */
101
+ collab?: CollabOptions;
102
+ /** 是否启用代码高亮 */
103
+ highlight?: boolean | HighlightOptions;
104
+ /** 主题配置 */
105
+ theme?: ThemeConfig;
106
+ /** 额外 Milkdown 插件 */
107
+ plugins?: MilkdownPlugin[];
108
+ /** 内容变更回调 */
109
+ onChange?: (markdown: string) => void;
110
+ /** 在线用户列表变化回调 */
111
+ onAwarenessChange?: (users: AwarenessUserState[]) => void;
112
+ }
113
+ /**
114
+ * CollaMarkdown 编辑器实例。
115
+ */
116
+ interface CollaMarkdownEditor {
117
+ /** 底层 Milkdown Editor 实例 */
118
+ readonly editor: Editor;
119
+ /** 协同内容源:以 Markdown 文本为唯一真相的 Yjs Y.Text(未启用协同时为本地文档) */
120
+ readonly content: Y.Text;
121
+ /** 获取底层 ProseMirror 视图(用于预览区光标定位) */
122
+ getView(): EditorView;
123
+ /** 获取当前 Markdown 内容 */
124
+ getMarkdown(): string;
125
+ /** 设置 Markdown 内容(会重置编辑器状态) */
126
+ setMarkdown(value: string): Promise<void>;
127
+ /** 获取当前在线用户列表(需启用协同) */
128
+ getAwarenessUsers(): AwarenessUserState[];
129
+ /** 上报本地选区(Y.Text 字符偏移),用于远端光标显示 */
130
+ setLocalSelection(anchor: number, head: number): void;
131
+ /** 获取远端光标列表(已解码为 Y.Text 绝对偏移,需启用协同) */
132
+ getRemoteCursors(): RemoteCursor[];
133
+ /** 销毁编辑器并清理资源 */
134
+ destroy(): Promise<void>;
135
+ }
136
+
137
+ /**
138
+ * 创建 CollaMarkdown 编辑器实例。
139
+ *
140
+ * Phase 1~4 实现能力:
141
+ * - 基于 Milkdown 渲染 Markdown
142
+ * - 接入 listener 插件,支持 onChange 回调
143
+ * - 默认启用 commonmark、prism 代码高亮、block 菜单
144
+ * - 以 Markdown 文本(Y.Text)为唯一真相的可选协同:源码区与预览区都读写同一份文本
145
+ * - 自定义布局节点(Callout / Columns / Column / Card)
146
+ * - CollaFlow 主题 CSS 变量
147
+ */
148
+ declare function createEditor(options: CollaMarkdownEditorOptions): Promise<CollaMarkdownEditor>;
149
+
150
+ /**
151
+ * CollaFlow 协同 Provider。
152
+ *
153
+ * 职责:
154
+ * - 维护 Y.Doc 与 Awareness 实例
155
+ * - 通过 @hocuspocus/provider 与 CollaFlow Core 的 Hocuspocus 服务通信
156
+ * - 暴露在线用户列表
157
+ *
158
+ * 传输层(Yjs sync + awareness 协议)完全交给 Hocuspocus,
159
+ * 本类只做生命周期与本地状态的收口。
160
+ */
161
+ declare class CollaFlowProvider {
162
+ readonly doc: Y.Doc;
163
+ readonly awareness: awarenessProtocol.Awareness;
164
+ private readonly provider;
165
+ private connected;
166
+ constructor(options: CollabOptions);
167
+ /**
168
+ * 当前是否已与协同服务建立连接。
169
+ */
170
+ get isConnected(): boolean;
171
+ /**
172
+ * 协同内容源:以 Markdown 文本为唯一真相的 Y.Text。
173
+ *
174
+ * 源码区与预览区都读写这一份文本;协同(Hocuspocus)只负责同步它,
175
+ * 因此多人编辑按字符级 CRDT 合并,不会再出现「整体替换」式的覆盖。
176
+ */
177
+ get content(): Y.Text;
178
+ /**
179
+ * 获取当前在线用户列表。
180
+ */
181
+ getAwarenessUsers(): AwarenessUserState[];
182
+ /**
183
+ * 建立连接。
184
+ */
185
+ connect(): void;
186
+ /**
187
+ * 上报本地选区(以 Y.Text 字符偏移表示)。
188
+ *
189
+ * 会被编码为相对位置存入 awareness,随并发编辑自愈,避免光标错位。
190
+ */
191
+ setLocalSelection(anchor: number, head: number): void;
192
+ /**
193
+ * 读取远端用户的光标列表(已解码为 Y.Text 绝对偏移)。
194
+ */
195
+ getRemoteCursors(): RemoteCursor[];
196
+ /**
197
+ * 断开连接(保留本地 doc 内容,可再次 connect)。
198
+ */
199
+ disconnect(): void;
200
+ /**
201
+ * 销毁 Provider,清理连接与 Yjs 资源。
202
+ */
203
+ destroy(): void;
204
+ }
205
+
206
+ /** Milkdown 编辑回写 Y.Text 时使用的 transaction origin。 */
207
+ declare const YJS_ORIGIN_MILKDOWN = "colla-md:milkdown";
208
+ /** 源码区(textarea)编辑写入 Y.Text 时使用的 transaction origin。 */
209
+ declare const YJS_ORIGIN_TEXTAREA = "colla-md:textarea";
210
+ /**
211
+ * 计算最小差异并将 newText 应用到 yText(字符级增删)。
212
+ *
213
+ * 不采用整体「删除全部 + 重新插入」,否则会与并发的远端编辑冲突(整体替换会清掉他人刚插入的内容)。
214
+ * 通过 transaction origin 标记来源,便于观察者区分「自己写的」与「远端 / 其他端写的」。
215
+ */
216
+ declare function diffApply(yText: Y.Text, newText: string, origin: unknown): void;
217
+ declare function createCollabPlugins(content: Y.Text): MilkdownPlugin;
218
+
219
+ /**
220
+ * 预览区(Milkdown / ProseMirror)是富文本,而协同真相是 Y.Text(Markdown 纯文本)。
221
+ * 远端光标要落进预览区,需要知道「Markdown 第 o 个字符」在渲染后的 DOM 里对应第几个「真实文本字符」。
222
+ *
223
+ * 本函数同步遍历 ProseMirror 文档与 Markdown 字符串,统计每个 Markdown 下标之前有多少字符是
224
+ * 「真实 DOM 文本」(即去掉 `#`/`*`/反引号/换行等语法字符后的内容)。返回的数组长度为
225
+ * markdown.length + 1,counts[i] = markdown[0..i) 中的真实文本字符数。
226
+ *
227
+ * 配合在预览容器里按「真实文本字符偏移」定位 DOM 节点,即可用 getBoundingClientRect 算出远端光标坐标,
228
+ * 无需把 Markdown 偏移映射到 ProseMirror 坐标(那需要解析序列化格式,更脆弱)。
229
+ *
230
+ * 注:覆盖 commonmark 常见节点;自定义布局节点(Callout/Columns 等)按纯文本近似处理,光标可能略有偏差。
231
+ */
232
+ declare function buildDomTextCounts(doc: Node, markdown: string): number[];
233
+ /**
234
+ * 将 Y.Text 偏移转换为「真实 DOM 文本偏移」。
235
+ */
236
+ declare function markdownOffsetToDomTextOffset(counts: number[], offset: number): number;
237
+
238
+ export { type AwarenessUserInfo, CollaFlowProvider, type CollaMarkdownEditor, type CollaMarkdownEditorOptions, type CollabOptions, type EncodedSelection, type HighlightOptions, type RemoteCursor, type ThemeConfig, YJS_ORIGIN_MILKDOWN, YJS_ORIGIN_TEXTAREA, buildDomTextCounts, createCollabPlugins, createEditor, decodeOffset, decodeSelection, diffApply, encodeRelativePosition, markdownOffsetToDomTextOffset };
@@ -0,0 +1,238 @@
1
+ import { Editor } from '@milkdown/core';
2
+ import { MilkdownPlugin } from '@milkdown/ctx';
3
+ import * as Y from 'yjs';
4
+ import { EditorView } from 'prosemirror-view';
5
+ import * as awarenessProtocol from 'y-protocols/awareness';
6
+ import { Node } from 'prosemirror-model';
7
+
8
+ /**
9
+ * awareness 中携带的远端选区(已序列化为字符串,便于 JSON 传输)。
10
+ */
11
+ interface EncodedSelection {
12
+ /** 选区起点(Y.Text 相对位置,base64) */
13
+ anchor: string;
14
+ /** 选区终点(Y.Text 相对位置,base64) */
15
+ head: string;
16
+ }
17
+ /**
18
+ * 解析后的远端光标(已换算为 Y.Text 绝对偏移)。
19
+ */
20
+ interface RemoteCursor {
21
+ /** Yjs 客户端 ID */
22
+ clientId: number;
23
+ /** 用户信息(名称 / 颜色) */
24
+ user: AwarenessUserInfo;
25
+ /** 选区起点在 Y.Text 中的字符偏移 */
26
+ anchor: number;
27
+ /** 选区终点在 Y.Text 中的字符偏移 */
28
+ head: number;
29
+ }
30
+ /**
31
+ * 将 Y.Text 的字符偏移编码为可传输的相对位置。
32
+ *
33
+ * 相对位置记录的是「哪一项」,而非「第几个字符」,因此并发插入导致下标漂移时仍能自愈,
34
+ * 这是协同光标不会错位的关键。
35
+ */
36
+ declare function encodeRelativePosition(yText: Y.Text, index: number): string;
37
+ /**
38
+ * 将编码后的相对位置解码为当前文档下的绝对偏移。
39
+ * 若该位置已随编辑被移除(超出文档范围),返回 null。
40
+ */
41
+ declare function decodeOffset(yText: Y.Text, encoded: string): number | null;
42
+ /**
43
+ * 解码远端选区为 Y.Text 绝对偏移。
44
+ */
45
+ declare function decodeSelection(yText: Y.Text, selection: EncodedSelection): {
46
+ anchor: number;
47
+ head: number;
48
+ } | null;
49
+
50
+ /**
51
+ * 协同用户信息。
52
+ */
53
+ interface AwarenessUserInfo {
54
+ /** 显示名称 */
55
+ name: string;
56
+ /** 用户颜色(HEX 格式) */
57
+ color: string;
58
+ /** 头像 URL(可选) */
59
+ avatar?: string;
60
+ }
61
+ /**
62
+ * 在线用户状态。
63
+ */
64
+ interface AwarenessUserState {
65
+ /** Yjs 客户端 ID */
66
+ clientId: number;
67
+ /** 用户信息 */
68
+ user: AwarenessUserInfo;
69
+ }
70
+ /**
71
+ * 协同配置。
72
+ */
73
+ interface CollabOptions {
74
+ /** 房间/文档 ID(对应 Hocuspocus 文档名) */
75
+ roomId: string;
76
+ /** CollaFlow Core(Hocuspocus 协同服务)的 WebSocket 地址,如 ws://localhost:1234 */
77
+ serverUrl: string;
78
+ /** 用户 Awareness 信息(用于光标/在线列表展示) */
79
+ user?: AwarenessUserInfo;
80
+ }
81
+ /**
82
+ * 代码高亮配置。
83
+ */
84
+ interface HighlightOptions {
85
+ /** 高亮器类型 */
86
+ type: 'prism' | 'shiki';
87
+ }
88
+ /**
89
+ * 主题配置。
90
+ */
91
+ type ThemeConfig = 'light' | 'dark';
92
+ /**
93
+ * CollaMarkdown 编辑器配置。
94
+ */
95
+ interface CollaMarkdownEditorOptions {
96
+ /** 挂载节点 */
97
+ root: HTMLElement;
98
+ /** 初始 Markdown 内容 */
99
+ defaultValue?: string;
100
+ /** 是否启用协同 */
101
+ collab?: CollabOptions;
102
+ /** 是否启用代码高亮 */
103
+ highlight?: boolean | HighlightOptions;
104
+ /** 主题配置 */
105
+ theme?: ThemeConfig;
106
+ /** 额外 Milkdown 插件 */
107
+ plugins?: MilkdownPlugin[];
108
+ /** 内容变更回调 */
109
+ onChange?: (markdown: string) => void;
110
+ /** 在线用户列表变化回调 */
111
+ onAwarenessChange?: (users: AwarenessUserState[]) => void;
112
+ }
113
+ /**
114
+ * CollaMarkdown 编辑器实例。
115
+ */
116
+ interface CollaMarkdownEditor {
117
+ /** 底层 Milkdown Editor 实例 */
118
+ readonly editor: Editor;
119
+ /** 协同内容源:以 Markdown 文本为唯一真相的 Yjs Y.Text(未启用协同时为本地文档) */
120
+ readonly content: Y.Text;
121
+ /** 获取底层 ProseMirror 视图(用于预览区光标定位) */
122
+ getView(): EditorView;
123
+ /** 获取当前 Markdown 内容 */
124
+ getMarkdown(): string;
125
+ /** 设置 Markdown 内容(会重置编辑器状态) */
126
+ setMarkdown(value: string): Promise<void>;
127
+ /** 获取当前在线用户列表(需启用协同) */
128
+ getAwarenessUsers(): AwarenessUserState[];
129
+ /** 上报本地选区(Y.Text 字符偏移),用于远端光标显示 */
130
+ setLocalSelection(anchor: number, head: number): void;
131
+ /** 获取远端光标列表(已解码为 Y.Text 绝对偏移,需启用协同) */
132
+ getRemoteCursors(): RemoteCursor[];
133
+ /** 销毁编辑器并清理资源 */
134
+ destroy(): Promise<void>;
135
+ }
136
+
137
+ /**
138
+ * 创建 CollaMarkdown 编辑器实例。
139
+ *
140
+ * Phase 1~4 实现能力:
141
+ * - 基于 Milkdown 渲染 Markdown
142
+ * - 接入 listener 插件,支持 onChange 回调
143
+ * - 默认启用 commonmark、prism 代码高亮、block 菜单
144
+ * - 以 Markdown 文本(Y.Text)为唯一真相的可选协同:源码区与预览区都读写同一份文本
145
+ * - 自定义布局节点(Callout / Columns / Column / Card)
146
+ * - CollaFlow 主题 CSS 变量
147
+ */
148
+ declare function createEditor(options: CollaMarkdownEditorOptions): Promise<CollaMarkdownEditor>;
149
+
150
+ /**
151
+ * CollaFlow 协同 Provider。
152
+ *
153
+ * 职责:
154
+ * - 维护 Y.Doc 与 Awareness 实例
155
+ * - 通过 @hocuspocus/provider 与 CollaFlow Core 的 Hocuspocus 服务通信
156
+ * - 暴露在线用户列表
157
+ *
158
+ * 传输层(Yjs sync + awareness 协议)完全交给 Hocuspocus,
159
+ * 本类只做生命周期与本地状态的收口。
160
+ */
161
+ declare class CollaFlowProvider {
162
+ readonly doc: Y.Doc;
163
+ readonly awareness: awarenessProtocol.Awareness;
164
+ private readonly provider;
165
+ private connected;
166
+ constructor(options: CollabOptions);
167
+ /**
168
+ * 当前是否已与协同服务建立连接。
169
+ */
170
+ get isConnected(): boolean;
171
+ /**
172
+ * 协同内容源:以 Markdown 文本为唯一真相的 Y.Text。
173
+ *
174
+ * 源码区与预览区都读写这一份文本;协同(Hocuspocus)只负责同步它,
175
+ * 因此多人编辑按字符级 CRDT 合并,不会再出现「整体替换」式的覆盖。
176
+ */
177
+ get content(): Y.Text;
178
+ /**
179
+ * 获取当前在线用户列表。
180
+ */
181
+ getAwarenessUsers(): AwarenessUserState[];
182
+ /**
183
+ * 建立连接。
184
+ */
185
+ connect(): void;
186
+ /**
187
+ * 上报本地选区(以 Y.Text 字符偏移表示)。
188
+ *
189
+ * 会被编码为相对位置存入 awareness,随并发编辑自愈,避免光标错位。
190
+ */
191
+ setLocalSelection(anchor: number, head: number): void;
192
+ /**
193
+ * 读取远端用户的光标列表(已解码为 Y.Text 绝对偏移)。
194
+ */
195
+ getRemoteCursors(): RemoteCursor[];
196
+ /**
197
+ * 断开连接(保留本地 doc 内容,可再次 connect)。
198
+ */
199
+ disconnect(): void;
200
+ /**
201
+ * 销毁 Provider,清理连接与 Yjs 资源。
202
+ */
203
+ destroy(): void;
204
+ }
205
+
206
+ /** Milkdown 编辑回写 Y.Text 时使用的 transaction origin。 */
207
+ declare const YJS_ORIGIN_MILKDOWN = "colla-md:milkdown";
208
+ /** 源码区(textarea)编辑写入 Y.Text 时使用的 transaction origin。 */
209
+ declare const YJS_ORIGIN_TEXTAREA = "colla-md:textarea";
210
+ /**
211
+ * 计算最小差异并将 newText 应用到 yText(字符级增删)。
212
+ *
213
+ * 不采用整体「删除全部 + 重新插入」,否则会与并发的远端编辑冲突(整体替换会清掉他人刚插入的内容)。
214
+ * 通过 transaction origin 标记来源,便于观察者区分「自己写的」与「远端 / 其他端写的」。
215
+ */
216
+ declare function diffApply(yText: Y.Text, newText: string, origin: unknown): void;
217
+ declare function createCollabPlugins(content: Y.Text): MilkdownPlugin;
218
+
219
+ /**
220
+ * 预览区(Milkdown / ProseMirror)是富文本,而协同真相是 Y.Text(Markdown 纯文本)。
221
+ * 远端光标要落进预览区,需要知道「Markdown 第 o 个字符」在渲染后的 DOM 里对应第几个「真实文本字符」。
222
+ *
223
+ * 本函数同步遍历 ProseMirror 文档与 Markdown 字符串,统计每个 Markdown 下标之前有多少字符是
224
+ * 「真实 DOM 文本」(即去掉 `#`/`*`/反引号/换行等语法字符后的内容)。返回的数组长度为
225
+ * markdown.length + 1,counts[i] = markdown[0..i) 中的真实文本字符数。
226
+ *
227
+ * 配合在预览容器里按「真实文本字符偏移」定位 DOM 节点,即可用 getBoundingClientRect 算出远端光标坐标,
228
+ * 无需把 Markdown 偏移映射到 ProseMirror 坐标(那需要解析序列化格式,更脆弱)。
229
+ *
230
+ * 注:覆盖 commonmark 常见节点;自定义布局节点(Callout/Columns 等)按纯文本近似处理,光标可能略有偏差。
231
+ */
232
+ declare function buildDomTextCounts(doc: Node, markdown: string): number[];
233
+ /**
234
+ * 将 Y.Text 偏移转换为「真实 DOM 文本偏移」。
235
+ */
236
+ declare function markdownOffsetToDomTextOffset(counts: number[], offset: number): number;
237
+
238
+ export { type AwarenessUserInfo, CollaFlowProvider, type CollaMarkdownEditor, type CollaMarkdownEditorOptions, type CollabOptions, type EncodedSelection, type HighlightOptions, type RemoteCursor, type ThemeConfig, YJS_ORIGIN_MILKDOWN, YJS_ORIGIN_TEXTAREA, buildDomTextCounts, createCollabPlugins, createEditor, decodeOffset, decodeSelection, diffApply, encodeRelativePosition, markdownOffsetToDomTextOffset };