UNPKG

@milkdown/core

Version:
695 lines (694 loc) 23.7 kB
import { Clock, Container, Ctx, createSlice, createTimer } from "@milkdown/ctx"; import remarkParse from "remark-parse"; import remarkStringify from "remark-stringify"; import { unified } from "unified"; import { callCommandBeforeEditorView, ctxCallOutOfScope, docTypeError } from "@milkdown/exception"; import { baseKeymap, chainCommands, deleteSelection, joinTextblockBackward, selectNodeBackward } from "@milkdown/prose/commands"; import { DOMParser, Node, Schema } from "@milkdown/prose/model"; import { customInputRules } from "@milkdown/prose"; import { keymap as keymap$1 } from "@milkdown/prose/keymap"; import { EditorState, Plugin, PluginKey } from "@milkdown/prose/state"; import { undoInputRule } from "@milkdown/prose/inputrules"; import { ParserState, SerializerState } from "@milkdown/transformer"; import { EditorView } from "@milkdown/prose/view"; //#region src/__internal__/utils.ts function withMeta(plugin, meta) { plugin.meta = { package: "@milkdown/core", group: "System", ...meta }; return plugin; } //#endregion //#region src/__internal__/remark-handlers.ts var remarkHandlers = { text: (node, _, state, info) => { const value = node.value; if (/^[^*_\\]*\s+$/.test(value)) return value; return state.safe(value, { ...info, encode: [] }); }, strong: (node, _, state, info) => { const marker = node.marker || state.options.strong || "*"; const exit = state.enter("strong"); const tracker = state.createTracker(info); let value = tracker.move(marker + marker); value += tracker.move(state.containerPhrasing(node, { before: value, after: marker, ...tracker.current() })); value += tracker.move(marker + marker); exit(); return value; }, emphasis: (node, _, state, info) => { const marker = node.marker || state.options.emphasis || "*"; const exit = state.enter("emphasis"); const tracker = state.createTracker(info); let value = tracker.move(marker); value += tracker.move(state.containerPhrasing(node, { before: value, after: marker, ...tracker.current() })); value += tracker.move(marker); exit(); return value; } }; //#endregion //#region src/internal-plugin/atoms.ts var editorViewCtx = createSlice({}, "editorView"); var editorStateCtx = createSlice({}, "editorState"); var initTimerCtx = createSlice([], "initTimer"); var editorCtx = createSlice({}, "editor"); var inputRulesCtx = createSlice([], "inputRules"); var prosePluginsCtx = createSlice([], "prosePlugins"); var remarkPluginsCtx = createSlice([], "remarkPlugins"); var nodeViewCtx = createSlice([], "nodeView"); var markViewCtx = createSlice([], "markView"); var remarkCtx = createSlice(unified().use(remarkParse).use(remarkStringify), "remark"); var remarkStringifyOptionsCtx = createSlice({ handlers: remarkHandlers, encode: [] }, "remarkStringifyOptions"); //#endregion //#region src/internal-plugin/config.ts var ConfigReady = createTimer("ConfigReady"); function config(configure) { const plugin = (ctx) => { ctx.record(ConfigReady); return async () => { await configure(ctx); ctx.done(ConfigReady); return () => { ctx.clearTimer(ConfigReady); }; }; }; withMeta(plugin, { displayName: "Config" }); return plugin; } //#endregion //#region src/internal-plugin/init.ts var InitReady = createTimer("InitReady"); function init(editor) { const plugin = (ctx) => { ctx.inject(editorCtx, editor).inject(prosePluginsCtx, []).inject(remarkPluginsCtx, []).inject(inputRulesCtx, []).inject(nodeViewCtx, []).inject(markViewCtx, []).inject(remarkStringifyOptionsCtx, { handlers: remarkHandlers, encode: [] }).inject(remarkCtx, unified().use(remarkParse).use(remarkStringify)).inject(initTimerCtx, [ConfigReady]).record(InitReady); return async () => { await ctx.waitTimers(initTimerCtx); const options = ctx.get(remarkStringifyOptionsCtx); ctx.set(remarkCtx, unified().use(remarkParse).use(remarkStringify, options)); ctx.done(InitReady); return () => { ctx.remove(editorCtx).remove(prosePluginsCtx).remove(remarkPluginsCtx).remove(inputRulesCtx).remove(nodeViewCtx).remove(markViewCtx).remove(remarkStringifyOptionsCtx).remove(remarkCtx).remove(initTimerCtx).clearTimer(InitReady); }; }; }; withMeta(plugin, { displayName: "Init" }); return plugin; } //#endregion //#region src/internal-plugin/schema.ts var SchemaReady = createTimer("SchemaReady"); var schemaTimerCtx = createSlice([], "schemaTimer"); var schemaCtx = createSlice({}, "schema"); var nodesCtx = createSlice([], "nodes"); var marksCtx = createSlice([], "marks"); function extendPriority(x) { return { ...x, parseDOM: x.parseDOM?.map((rule) => ({ priority: x.priority, ...rule })) }; } var schema = (ctx) => { ctx.inject(schemaCtx, {}).inject(nodesCtx, []).inject(marksCtx, []).inject(schemaTimerCtx, [InitReady]).record(SchemaReady); return async () => { await ctx.waitTimers(schemaTimerCtx); const remark = ctx.get(remarkCtx); const processor = ctx.get(remarkPluginsCtx).reduce((acc, plug) => acc.use(plug.plugin, plug.options), remark); ctx.set(remarkCtx, processor); const schema = new Schema({ nodes: Object.fromEntries(ctx.get(nodesCtx).map(([key, x]) => [key, extendPriority(x)])), marks: Object.fromEntries(ctx.get(marksCtx).map(([key, x]) => [key, extendPriority(x)])) }); ctx.set(schemaCtx, schema); ctx.done(SchemaReady); return () => { ctx.remove(schemaCtx).remove(nodesCtx).remove(marksCtx).remove(schemaTimerCtx).clearTimer(SchemaReady); }; }; }; withMeta(schema, { displayName: "Schema" }); //#endregion //#region src/internal-plugin/commands.ts var CommandManager = class { constructor() { this.#container = new Container(); this.#ctx = null; this.setCtx = (ctx) => { this.#ctx = ctx; }; this.chain = () => { if (this.#ctx == null) throw callCommandBeforeEditorView(); const ctx = this.#ctx; const commands = []; const get = this.get.bind(this); const chains = { run: () => { const chained = chainCommands(...commands); const view = ctx.get(editorViewCtx); return chained(view.state, view.dispatch, view); }, inline: (command) => { commands.push(command); return chains; }, pipe: pipe.bind(this) }; function pipe(slice, payload) { const cmd = get(slice); commands.push(cmd(payload)); return chains; } return chains; }; } #container; #ctx; get ctx() { return this.#ctx; } create(meta, value) { const slice = meta.create(this.#container.sliceMap); slice.set(value); return slice; } get(slice) { return this.#container.get(slice).get(); } remove(slice) { return this.#container.remove(slice); } call(slice, payload) { if (this.#ctx == null) throw callCommandBeforeEditorView(); const command = this.get(slice)(payload); const view = this.#ctx.get(editorViewCtx); return command(view.state, view.dispatch, view); } inline(command) { if (this.#ctx == null) throw callCommandBeforeEditorView(); const view = this.#ctx.get(editorViewCtx); return command(view.state, view.dispatch, view); } }; function createCmdKey(key = "cmdKey") { return createSlice((() => () => false), key); } var commandsCtx = createSlice(new CommandManager(), "commands"); var commandsTimerCtx = createSlice([SchemaReady], "commandsTimer"); var CommandsReady = createTimer("CommandsReady"); var commands = (ctx) => { const cmd = new CommandManager(); cmd.setCtx(ctx); ctx.inject(commandsCtx, cmd).inject(commandsTimerCtx, [SchemaReady]).record(CommandsReady); return async () => { await ctx.waitTimers(commandsTimerCtx); ctx.done(CommandsReady); return () => { ctx.remove(commandsCtx).remove(commandsTimerCtx).clearTimer(CommandsReady); }; }; }; withMeta(commands, { displayName: "Commands" }); //#endregion //#region src/internal-plugin/keymap.ts function overrideBaseKeymap(keymap) { keymap.Backspace = chainCommands(undoInputRule, deleteSelection, joinTextblockBackward, selectNodeBackward); return keymap; } var KeymapManager = class { constructor() { this.#ctx = null; this.#keymap = []; this.setCtx = (ctx) => { this.#ctx = ctx; }; this.add = (keymap) => { this.#keymap.push(keymap); return () => { this.#keymap = this.#keymap.filter((item) => item !== keymap); }; }; this.addObjectKeymap = (keymaps) => { const remove = []; Object.entries(keymaps).forEach(([key, command]) => { if (typeof command === "function") { const keymapItem = { key, onRun: () => command }; this.#keymap.push(keymapItem); remove.push(() => { this.#keymap = this.#keymap.filter((item) => item !== keymapItem); }); } else { this.#keymap.push(command); remove.push(() => { this.#keymap = this.#keymap.filter((item) => item !== command); }); } }); return () => { remove.forEach((fn) => fn()); }; }; this.addBaseKeymap = () => { const base = overrideBaseKeymap(baseKeymap); return this.addObjectKeymap(base); }; this.build = () => { const keymap = {}; this.#keymap.forEach((item) => { keymap[item.key] = [...keymap[item.key] || [], item]; }); return Object.fromEntries(Object.entries(keymap).map(([key, items]) => { const sortedItems = items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50)); const command = (state, dispatch, view) => { const ctx = this.#ctx; if (ctx == null) throw ctxCallOutOfScope(); return chainCommands(...sortedItems.map((item) => item.onRun(ctx)))(state, dispatch, view); }; return [key, command]; })); }; } #ctx; #keymap; get ctx() { return this.#ctx; } }; var keymapCtx = createSlice(new KeymapManager(), "keymap"); var keymapTimerCtx = createSlice([SchemaReady], "keymapTimer"); var KeymapReady = createTimer("KeymapReady"); var keymap = (ctx) => { const km = new KeymapManager(); km.setCtx(ctx); ctx.inject(keymapCtx, km).inject(keymapTimerCtx, [SchemaReady]).record(KeymapReady); return async () => { await ctx.waitTimers(keymapTimerCtx); ctx.done(KeymapReady); return () => { ctx.remove(keymapCtx).remove(keymapTimerCtx).clearTimer(KeymapReady); }; }; }; //#endregion //#region src/internal-plugin/parser.ts var ParserReady = createTimer("ParserReady"); var outOfScope$1 = (() => { throw ctxCallOutOfScope(); }); var parserCtx = createSlice(outOfScope$1, "parser"); var parserTimerCtx = createSlice([], "parserTimer"); var parser = (ctx) => { ctx.inject(parserCtx, outOfScope$1).inject(parserTimerCtx, [SchemaReady]).record(ParserReady); return async () => { await ctx.waitTimers(parserTimerCtx); const remark = ctx.get(remarkCtx); const schema = ctx.get(schemaCtx); ctx.set(parserCtx, ParserState.create(schema, remark)); ctx.done(ParserReady); return () => { ctx.remove(parserCtx).remove(parserTimerCtx).clearTimer(ParserReady); }; }; }; withMeta(parser, { displayName: "Parser" }); //#endregion //#region src/internal-plugin/serializer.ts var SerializerReady = createTimer("SerializerReady"); var serializerTimerCtx = createSlice([], "serializerTimer"); var outOfScope = (() => { throw ctxCallOutOfScope(); }); var serializerCtx = createSlice(outOfScope, "serializer"); var serializer = (ctx) => { ctx.inject(serializerCtx, outOfScope).inject(serializerTimerCtx, [SchemaReady]).record(SerializerReady); return async () => { await ctx.waitTimers(serializerTimerCtx); const remark = ctx.get(remarkCtx); const schema = ctx.get(schemaCtx); ctx.set(serializerCtx, SerializerState.create(schema, remark)); ctx.done(SerializerReady); return () => { ctx.remove(serializerCtx).remove(serializerTimerCtx).clearTimer(SerializerReady); }; }; }; withMeta(serializer, { displayName: "Serializer" }); //#endregion //#region src/internal-plugin/editor-state.ts var defaultValueCtx = createSlice("", "defaultValue"); var editorStateOptionsCtx = createSlice((x) => x, "stateOptions"); var editorStateTimerCtx = createSlice([], "editorStateTimer"); var EditorStateReady = createTimer("EditorStateReady"); function getDoc(defaultValue, parser, schema) { if (typeof defaultValue === "string") return parser(defaultValue); if (defaultValue.type === "html") return DOMParser.fromSchema(schema).parse(defaultValue.dom); if (defaultValue.type === "json") return Node.fromJSON(schema, defaultValue.value); throw docTypeError(defaultValue); } var key$1 = new PluginKey("MILKDOWN_STATE_TRACKER"); var editorState = (ctx) => { ctx.inject(defaultValueCtx, "").inject(editorStateCtx, {}).inject(editorStateOptionsCtx, (x) => x).inject(editorStateTimerCtx, [ ParserReady, SerializerReady, CommandsReady, KeymapReady ]).record(EditorStateReady); return async () => { await ctx.waitTimers(editorStateTimerCtx); const schema = ctx.get(schemaCtx); const parser = ctx.get(parserCtx); const rules = ctx.get(inputRulesCtx); const optionsOverride = ctx.get(editorStateOptionsCtx); const prosePlugins = ctx.get(prosePluginsCtx); const doc = getDoc(ctx.get(defaultValueCtx), parser, schema); const km = ctx.get(keymapCtx); const disposeBaseKeymap = km.addBaseKeymap(); const plugins = [ ...prosePlugins, new Plugin({ key: key$1, state: { init: () => {}, apply: (_tr, _value, _oldState, newState) => { ctx.set(editorStateCtx, newState); } } }), customInputRules({ rules }), keymap$1(km.build()) ]; ctx.set(prosePluginsCtx, plugins); const options = optionsOverride({ schema, doc, plugins }); const state = EditorState.create(options); ctx.set(editorStateCtx, state); ctx.done(EditorStateReady); return () => { disposeBaseKeymap(); ctx.remove(defaultValueCtx).remove(editorStateCtx).remove(editorStateOptionsCtx).remove(editorStateTimerCtx).clearTimer(EditorStateReady); }; }; }; withMeta(editorState, { displayName: "EditorState" }); //#endregion //#region src/internal-plugin/paste-rule.ts var pasteRulesCtx = createSlice([], "pasteRule"); var pasteRulesTimerCtx = createSlice([SchemaReady], "pasteRuleTimer"); var PasteRulesReady = createTimer("PasteRuleReady"); var pasteRule = (ctx) => { ctx.inject(pasteRulesCtx, []).inject(pasteRulesTimerCtx, [SchemaReady]).record(PasteRulesReady); return async () => { await ctx.waitTimers(pasteRulesTimerCtx); ctx.done(PasteRulesReady); return () => { ctx.remove(pasteRulesCtx).remove(pasteRulesTimerCtx).clearTimer(PasteRulesReady); }; }; }; withMeta(pasteRule, { displayName: "PasteRule" }); //#endregion //#region src/internal-plugin/editor-view.ts var EditorViewReady = createTimer("EditorViewReady"); var editorViewTimerCtx = createSlice([], "editorViewTimer"); var editorViewOptionsCtx = createSlice({}, "editorViewOptions"); var rootCtx = createSlice(null, "root"); var rootDOMCtx = createSlice(null, "rootDOM"); var rootAttrsCtx = createSlice({}, "rootAttrs"); function createViewContainer(root, ctx) { const container = document.createElement("div"); container.className = "milkdown"; root.appendChild(container); ctx.set(rootDOMCtx, container); const attrs = ctx.get(rootAttrsCtx); Object.entries(attrs).forEach(([key, value]) => container.setAttribute(key, value)); return container; } function prepareViewDom(dom) { dom.classList.add("editor"); dom.setAttribute("role", "textbox"); } var key = new PluginKey("MILKDOWN_VIEW_CLEAR"); var editorView = (ctx) => { ctx.inject(rootCtx, document.body).inject(editorViewCtx, {}).inject(editorViewOptionsCtx, {}).inject(rootDOMCtx, null).inject(rootAttrsCtx, {}).inject(editorViewTimerCtx, [EditorStateReady, PasteRulesReady]).record(EditorViewReady); return async () => { await ctx.wait(InitReady); const root = ctx.get(rootCtx) || document.body; const el = typeof root === "string" ? document.querySelector(root) : root; ctx.update(prosePluginsCtx, (xs) => [new Plugin({ key, view: (editorView) => { const container = el ? createViewContainer(el, ctx) : void 0; const handleDOM = () => { if (container && el) { const editor = editorView.dom; el.replaceChild(container, editor); container.appendChild(editor); } }; handleDOM(); return { destroy: () => { if (container?.parentNode) container?.parentNode.replaceChild(editorView.dom, container); container?.remove(); } }; } }), ...xs]); await ctx.waitTimers(editorViewTimerCtx); const state = ctx.get(editorStateCtx); const options = ctx.get(editorViewOptionsCtx); const view = new EditorView(el, { state, nodeViews: Object.fromEntries(ctx.get(nodeViewCtx)), markViews: Object.fromEntries(ctx.get(markViewCtx)), transformPasted: (slice, view, isPlainText) => { ctx.get(pasteRulesCtx).sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50)).map((rule) => rule.run).forEach((runner) => { slice = runner(slice, view, isPlainText); }); return slice; }, ...options }); prepareViewDom(view.dom); ctx.set(editorViewCtx, view); ctx.done(EditorViewReady); return () => { view?.destroy(); ctx.remove(rootCtx).remove(editorViewCtx).remove(editorViewOptionsCtx).remove(rootDOMCtx).remove(rootAttrsCtx).remove(editorViewTimerCtx).clearTimer(EditorViewReady); }; }; }; withMeta(editorView, { displayName: "EditorView" }); //#endregion //#region src/editor/editor.ts var EditorStatus = /* @__PURE__ */ function(EditorStatus) { EditorStatus["Idle"] = "Idle"; EditorStatus["OnCreate"] = "OnCreate"; EditorStatus["Created"] = "Created"; EditorStatus["OnDestroy"] = "OnDestroy"; EditorStatus["Destroyed"] = "Destroyed"; return EditorStatus; }({}); var Editor = class Editor { constructor() { this.#enableInspector = false; this.#status = EditorStatus.Idle; this.#configureList = []; this.#onStatusChange = () => void 0; this.#container = new Container(); this.#clock = new Clock(); this.#usrPluginStore = /* @__PURE__ */ new Map(); this.#sysPluginStore = /* @__PURE__ */ new Map(); this.#ctx = new Ctx(this.#container, this.#clock); this.#loadInternal = () => { const configPlugin = config(async (ctx) => { await Promise.all(this.#configureList.map((fn) => Promise.resolve(fn(ctx)))); }); const internalPlugins = [ schema, parser, serializer, commands, keymap, pasteRule, editorState, editorView, init(this), configPlugin ]; this.#prepare(internalPlugins, this.#sysPluginStore); }; this.#prepare = (plugins, store) => { plugins.forEach((plugin) => { const ctx = this.#ctx.produce(this.#enableInspector ? plugin.meta : void 0); const handler = plugin(ctx); store.set(plugin, { ctx, handler, cleanup: void 0 }); }); }; this.#cleanup = (plugins, remove = false) => { return Promise.all([plugins].flat().map(async (plugin) => { const cleanup = this.#usrPluginStore.get(plugin)?.cleanup; if (remove) this.#usrPluginStore.delete(plugin); else this.#usrPluginStore.set(plugin, { ctx: void 0, handler: void 0, cleanup: void 0 }); if (typeof cleanup === "function") return cleanup(); return cleanup; })); }; this.#cleanupInternal = async () => { await Promise.all([...this.#sysPluginStore.entries()].map(async ([_, { cleanup }]) => { if (typeof cleanup === "function") return cleanup(); return cleanup; })); this.#sysPluginStore.clear(); }; this.#setStatus = (status) => { this.#status = status; this.#onStatusChange(status); }; this.#loadPluginInStore = (store) => { return [...store.entries()].map(async ([key, loader]) => { const { ctx, handler } = loader; if (!handler) return; const cleanup = await handler(); store.set(key, { ctx, handler, cleanup }); }); }; this.enableInspector = (enable = true) => { this.#enableInspector = enable; return this; }; this.onStatusChange = (onChange) => { this.#onStatusChange = onChange; return this; }; this.config = (configure) => { this.#configureList.push(configure); return this; }; this.removeConfig = (configure) => { this.#configureList = this.#configureList.filter((x) => x !== configure); return this; }; this.use = (plugins) => { const _plugins = [plugins].flat(); _plugins.flat().forEach((plugin) => { this.#usrPluginStore.set(plugin, { ctx: void 0, handler: void 0, cleanup: void 0 }); }); if (this.#status === EditorStatus.Created) this.#prepare(_plugins, this.#usrPluginStore); return this; }; this.remove = async (plugins) => { if (this.#status === EditorStatus.OnCreate) { console.warn("[Milkdown]: You are trying to remove plugins when the editor is creating, this is not recommended, please check your code."); return new Promise((resolve) => { setTimeout(() => { resolve(this.remove(plugins)); }, 50); }); } await this.#cleanup([plugins].flat(), true); return this; }; this.create = async () => { if (this.#status === EditorStatus.OnCreate) return this; if (this.#status === EditorStatus.Created) await this.destroy(); this.#setStatus(EditorStatus.OnCreate); this.#loadInternal(); this.#prepare([...this.#usrPluginStore.keys()], this.#usrPluginStore); await Promise.all([this.#loadPluginInStore(this.#sysPluginStore), this.#loadPluginInStore(this.#usrPluginStore)].flat()); this.#setStatus(EditorStatus.Created); return this; }; this.destroy = async (clearPlugins = false) => { if (this.#status === EditorStatus.Destroyed || this.#status === EditorStatus.OnDestroy) return this; if (this.#status === EditorStatus.OnCreate) return new Promise((resolve) => { setTimeout(() => { resolve(this.destroy(clearPlugins)); }, 50); }); if (clearPlugins) this.#configureList = []; this.#setStatus(EditorStatus.OnDestroy); await this.#cleanup([...this.#usrPluginStore.keys()], clearPlugins); await this.#cleanupInternal(); this.#setStatus(EditorStatus.Destroyed); return this; }; this.action = (action) => action(this.#ctx); this.inspect = () => { if (!this.#enableInspector) { console.warn("[Milkdown]: You are trying to collect inspection when inspector is disabled, please enable inspector by `editor.enableInspector()` first."); return []; } return [...this.#sysPluginStore.values(), ...this.#usrPluginStore.values()].map(({ ctx }) => ctx?.inspector?.read()).filter((x) => Boolean(x)); }; } static make() { return new Editor(); } #enableInspector; #status; #configureList; #onStatusChange; #container; #clock; #usrPluginStore; #sysPluginStore; #ctx; #loadInternal; #prepare; #cleanup; #cleanupInternal; #setStatus; #loadPluginInStore; get ctx() { return this.#ctx; } get status() { return this.#status; } }; //#endregion export { CommandManager, CommandsReady, ConfigReady, Editor, EditorStateReady, EditorStatus, EditorViewReady, InitReady, KeymapManager, KeymapReady, ParserReady, PasteRulesReady, SchemaReady, SerializerReady, commands, commandsCtx, commandsTimerCtx, config, createCmdKey, defaultValueCtx, editorCtx, editorState, editorStateCtx, editorStateOptionsCtx, editorStateTimerCtx, editorView, editorViewCtx, editorViewOptionsCtx, editorViewTimerCtx, getDoc, init, initTimerCtx, inputRulesCtx, keymap, keymapCtx, keymapTimerCtx, markViewCtx, marksCtx, nodeViewCtx, nodesCtx, parser, parserCtx, parserTimerCtx, pasteRule, pasteRulesCtx, pasteRulesTimerCtx, prosePluginsCtx, remarkCtx, remarkPluginsCtx, remarkStringifyOptionsCtx, rootAttrsCtx, rootCtx, rootDOMCtx, schema, schemaCtx, schemaTimerCtx, serializer, serializerCtx, serializerTimerCtx }; //# sourceMappingURL=index.js.map