Skip to content

Commit

Permalink
Fix markdown backend
Browse files Browse the repository at this point in the history
  • Loading branch information
zjkmxy committed Nov 1, 2024
1 parent 223ffad commit 95bdb77
Show file tree
Hide file tree
Showing 7 changed files with 455 additions and 202 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@material/web": "^2.2.0",
"@milkdown/core": "^7.5.0",
"@milkdown/ctx": "^7.5.0",
"@milkdown/exception": "^7.5.0",
"@milkdown/plugin-clipboard": "^7.5.0",
"@milkdown/plugin-collab": "^7.5.0",
"@milkdown/plugin-cursor": "^7.5.0",
Expand Down Expand Up @@ -88,6 +89,7 @@
"uuid": "^10.0.0",
"y-codemirror.next": "^0.3.5",
"y-prosemirror": "^1.2.12",
"y-protocols": "^1.0.6",
"yjs": "^13.6.20"
},
"devDependencies": {
Expand All @@ -104,7 +106,7 @@
"eslint-plugin-solid": "^0.14.3",
"peer": "^1.0.2",
"prettier": "^3.3.3",
"sass": "^1.80.4",
"sass": "^1.80.5",
"solid-devtools": "^0.30.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.12.2",
Expand Down
396 changes: 201 additions & 195 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/build-meta.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"revision":"v1.3.5 8abceae","timestamp":1730223476}
{"revision":"v1.3.6 21086d0","timestamp":1730446944}
246 changes: 246 additions & 0 deletions src/adaptors/milkdown-plugin-synced-store/collab-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import type { Ctx, MilkdownPlugin } from '@milkdown/ctx'
import { createSlice, createTimer } from '@milkdown/ctx'
import { DefaultValue, EditorViewReady } from '@milkdown/core'
import { editorViewCtx, getDoc, parserCtx, prosePluginsCtx, schemaCtx } from '@milkdown/core'
import { ctxNotBind, missingYjsDoc } from '@milkdown/exception'
import { keydownHandler } from '@milkdown/prose/keymap'
import type { Node } from '@milkdown/prose/model'
import { Plugin, PluginKey } from '@milkdown/prose/state'
import type { DecorationAttrs } from '@milkdown/prose/view'
import {
prosemirrorToYDoc,
redo,
undo,
yCursorPlugin,
yCursorPluginKey,
yXmlFragmentToProseMirrorRootNode,
ySyncPlugin,
ySyncPluginKey,
yUndoPlugin,
yUndoPluginKey,
} from 'y-prosemirror'
import type { Awareness } from 'y-protocols/awareness.js'
import type { PermanentUserData, XmlFragment } from 'yjs'
import { applyUpdate, encodeStateAsUpdate } from 'yjs'

/// @internal
export interface ColorDef {
light: string
dark: string
}

/// @internal
export interface YSyncOpts {
colors?: Array<ColorDef>
colorMapping?: Map<string, ColorDef>
permanentUserData?: PermanentUserData | null
}

/// @internal
export interface yCursorOpts {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cursorBuilder?: (arg: any) => HTMLElement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selectionBuilder?: (arg: any) => DecorationAttrs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSelection?: (arg: any) => unknown
}

/// @internal
export interface yUndoOpts {
protectedNodes?: Set<string>
trackedOrigins?: unknown[]
undoManager?: unknown
}

/// Options for the collab service.
export interface CollabServiceOptions {
/// The field name of the yCursor plugin.
yCursorStateField?: string

/// Options for the ySync plugin.
ySyncOpts?: YSyncOpts

/// Options for the yCursor plugin.
yCursorOpts?: yCursorOpts

/// Options for the yUndo plugin.
yUndoOpts?: yUndoOpts
}

/// @internal
export const CollabKeymapPluginKey = new PluginKey('MILKDOWN_COLLAB_KEYMAP')

const collabPluginKeys = [CollabKeymapPluginKey, ySyncPluginKey, yCursorPluginKey, yUndoPluginKey]

/// The collab service is used to manage the collaboration plugins.
/// It is used to provide the collaboration plugins to the editor.
export class CollabService {
/// @internal
#options: CollabServiceOptions = {}
/// @internal
#fragment?: XmlFragment
/// @internal
#awareness?: Awareness
/// @internal
#ctx?: Ctx
/// @internal
#connected = false

/// @internal
#valueToNode(value: DefaultValue): Node | undefined {
if (!this.#ctx) throw ctxNotBind()

const schema = this.#ctx.get(schemaCtx)
const parser = this.#ctx.get(parserCtx)

const doc = getDoc(value, parser, schema)
return doc
}

/// @internal
#createPlugins(): Plugin[] {
if (!this.#fragment) throw missingYjsDoc()
const { ySyncOpts, yUndoOpts } = this.#options
const type = this.#fragment
const plugins = [
ySyncPlugin(type, ySyncOpts),
yUndoPlugin(yUndoOpts),
new Plugin({
key: CollabKeymapPluginKey,
props: {
handleKeyDown: keydownHandler({
'Mod-z': undo,
'Mod-y': redo,
'Mod-Shift-z': redo,
}),
},
}),
]
if (this.#awareness) {
const { yCursorOpts, yCursorStateField } = this.#options
plugins.push(yCursorPlugin(this.#awareness, yCursorOpts as Required<yCursorOpts>, yCursorStateField))
}

return plugins
}

/// @internal
#flushEditor(plugins: Plugin[]) {
if (!this.#ctx) throw ctxNotBind()
this.#ctx.set(prosePluginsCtx, plugins)

const view = this.#ctx.get(editorViewCtx)
const newState = view.state.reconfigure({ plugins })
view.updateState(newState)
}

/// Bind the context to the service.
bindCtx(ctx: Ctx) {
this.#ctx = ctx
return this
}

/// Bind the document to the service.
bindFragment(fragment: XmlFragment) {
this.#fragment = fragment
return this
}

/// Set the options of the service.
setOptions(options: CollabServiceOptions) {
this.#options = options
return this
}

/// Merge some options to the service.
/// The options will be merged to the existing options.
/// THe options should be partial of the `CollabServiceOptions`.
mergeOptions(options: Partial<CollabServiceOptions>) {
Object.assign(this.#options, options)
return this
}

/// Set the awareness of the service.
setAwareness(awareness: Awareness) {
this.#awareness = awareness
return this
}

/// Apply the template to the document.
applyTemplate(template: DefaultValue, condition?: (yDocNode: Node, templateNode: Node) => boolean) {
if (!this.#ctx) throw ctxNotBind()
if (!this.#fragment) throw missingYjsDoc()
const conditionFn = condition || ((yDocNode) => yDocNode.textContent.length === 0)

const node = this.#valueToNode(template)
const schema = this.#ctx.get(schemaCtx)
const yDocNode = yXmlFragmentToProseMirrorRootNode(this.#fragment, schema)

if (node && conditionFn(yDocNode, node)) {
this.#fragment.delete(0, this.#fragment.length)
const templateDoc = prosemirrorToYDoc(node)
const template = encodeStateAsUpdate(templateDoc)
const parentYDoc = this.#fragment.doc
if (parentYDoc !== null) {
applyUpdate(parentYDoc, template)
}
templateDoc.destroy()
}

return this
}

/// Connect the service.
connect() {
if (!this.#ctx) throw ctxNotBind()
if (this.#connected) return

const prosePlugins = this.#ctx.get(prosePluginsCtx)
const collabPlugins = this.#createPlugins()
const plugins = prosePlugins.concat(collabPlugins)

this.#flushEditor(plugins)
this.#connected = true

return this
}

/// Disconnect the service.
disconnect() {
if (!this.#ctx) throw ctxNotBind()
if (!this.#connected) return this

const prosePlugins = this.#ctx.get(prosePluginsCtx)
const plugins = prosePlugins.filter((plugin) => !plugin.spec.key || !collabPluginKeys.includes(plugin.spec.key))

this.#flushEditor(plugins)
this.#connected = false

return this
}
}

/// A slice that contains the collab service.
export const collabServiceCtx = createSlice(new CollabService(), 'collabServiceCtx')

/// The timer that indicates the collab plugin is ready.
export const CollabReady = createTimer('CollabReady')

/// The collab plugin.
export const collab: MilkdownPlugin = (ctx) => {
const collabService = new CollabService()
ctx.inject(collabServiceCtx, collabService).record(CollabReady)
return async () => {
await ctx.wait(EditorViewReady)
collabService.bindCtx(ctx)
ctx.done(CollabReady)
return () => {
ctx.remove(collabServiceCtx).clearTimer(CollabReady)
}
}
}
collab.meta = {
package: '@milkdown/plugin-collab',
displayName: 'Collab',
}
4 changes: 2 additions & 2 deletions src/build-meta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const REVISION = 'v1.3.5 8abceae'
export const REVISION = 'v1.3.6 21086d0'
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision, prettier/prettier
export const TIMESTAMP = 1730223476
export const TIMESTAMP = 1730446944
4 changes: 2 additions & 2 deletions src/components/share-latex/markdown-doc/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createSignal, onCleanup, onMount } from 'solid-js'
import { Editor, rootCtx } from '@milkdown/core'
import { commonmark } from '@milkdown/preset-commonmark'
import { history } from '@milkdown/plugin-history'
import { collab, CollabService, collabServiceCtx } from '@milkdown/plugin-collab'
import { collab, CollabService, collabServiceCtx } from '../../../adaptors/milkdown-plugin-synced-store/collab-service'
import { nord } from '@milkdown/theme-nord'
import { cursor } from '@milkdown/plugin-cursor'
import { indent, indentConfig, IndentConfigOptions } from '@milkdown/plugin-indent'
Expand Down Expand Up @@ -52,7 +52,7 @@ export default function MarkdownDoc(props: {

collabSrv
// bind doc and awareness
.bindDoc(props.doc.doc!)
.bindFragment(props.doc)
.setAwareness(props.provider.awareness!)
// connect yjs with milkdown
.connect()
Expand Down
1 change: 0 additions & 1 deletion src/components/share-latex/rich-doc/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
FormatClear as FormatClearIcon,
Undo as UndoIcon,
Redo as RedoIcon,
Link as LinkIcon,
} from '@suid/icons-material'
import { H1Icon, H2Icon, H3Icon, H4Icon } from './icons'
import CmdIcon from './cmd-icon'
Expand Down

0 comments on commit 95bdb77

Please sign in to comment.