diff --git a/apps/playground/package.json b/apps/playground/package.json index 8d4dcbf..736950d 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -10,6 +10,8 @@ "astro": "astro" }, "dependencies": { + "@codemirror/state": "catalog:", + "@codemirror/view": "catalog:", "astro": "^7.1.6" }, "engines": { diff --git a/apps/playground/src/pages/index.astro b/apps/playground/src/pages/index.astro index 14ec1e7..29da09b 100644 --- a/apps/playground/src/pages/index.astro +++ b/apps/playground/src/pages/index.astro @@ -1,26 +1,57 @@ --- -import '../assets/global.css' +import "../assets/global.css"; --- - - - - - - - Inkwell Playground - - -
- + + + + + + + Inkwell Playground + + +
+ \ No newline at end of file + function dummyKeymap(tag) { + return keymap.of([ + { + key: "Enter", + run() { + console.log(this, tag); + return true; + }, + }, + ]); + } + + const ChildExtension: InkwellExtension = { + name: "child", + addCodeMirrorExtensions: () => [], + }; + + const TestExtension: InkwellExtension = { + name: "test", + priority: 10, + addExtensions: () => [ChildExtension], + addCodeMirrorExtensions: ({ editor }) => { + return [dummyKeymap("a")]; + }, + }; + + const editor = new Editor({ + element: document.getElementById("editor-mount")!, + content: "# This is the Inkwell playground\n\nFeel free to play around", + extensions: [TestExtension], + }); + + console.log(editor); + diff --git a/packages/core/src/editor/Editor.ts b/packages/core/src/editor/Editor.ts index 96b3ae0..c9ac5ac 100644 --- a/packages/core/src/editor/Editor.ts +++ b/packages/core/src/editor/Editor.ts @@ -2,10 +2,12 @@ import { EditorState, Text } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { EventEmitter } from "./EventEmitter.ts"; import type { EditorEvents, EditorOptions } from "./types.ts"; +import { ExtensionManager } from "../extensions/index.ts"; export class Editor extends EventEmitter { private _te: TextEncoder = new TextEncoder(); private _td: TextDecoder = new TextDecoder(); + private _extensionManager: ExtensionManager; options: EditorOptions; view: EditorView; @@ -54,6 +56,7 @@ export class Editor extends EventEmitter { super(); this.options = options; const { element, content } = this.options; + this._extensionManager = new ExtensionManager(this, this.options?.extensions ?? []); this.emit("beforeCreate", { editor: this }); @@ -64,6 +67,7 @@ export class Editor extends EventEmitter { this.view = new EditorView({ state: initialState, parent: element, + extensions: this._extensionManager.cmExtensions, }); this.setupDOM(); diff --git a/packages/core/src/editor/index.ts b/packages/core/src/editor/index.ts index a571e42..3443179 100644 --- a/packages/core/src/editor/index.ts +++ b/packages/core/src/editor/index.ts @@ -1 +1,3 @@ export * from "./Editor.ts"; +export * from "./EventEmitter.ts"; +export * from "./types.ts"; diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 5508206..8f31e5f 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -1,9 +1,11 @@ import type { EditorSelection, Transaction } from "@codemirror/state"; import type { Editor } from "./Editor.ts"; +import type { InkwellExtension } from "../extensions/types.ts"; export type EditorOptions = { element: HTMLElement; content?: string; + extensions?: InkwellExtension[]; }; export type EditorEvents = { diff --git a/packages/core/src/extensions/ExtensionManager.ts b/packages/core/src/extensions/ExtensionManager.ts new file mode 100644 index 0000000..e7261d7 --- /dev/null +++ b/packages/core/src/extensions/ExtensionManager.ts @@ -0,0 +1,75 @@ +import type { Extension as CMExtension } from "@codemirror/state"; +import type { Editor } from "../editor/Editor.ts"; +import type { InkwellExtension } from "./types.ts"; + +export class ExtensionManager { + private editor: Editor; + private _extensions: InkwellExtension[] = []; + private _resolvedExtensions: InkwellExtension[] = []; + private _cmExtensions: CMExtension[] = []; + + constructor(editor: Editor, extensions: InkwellExtension[]) { + this.editor = editor; + this._extensions = extensions; + this._resolvedExtensions = this.resolveExtensions(); + this._cmExtensions = this.toCodeMirrorExtensions(); + } + + get extensions(): InkwellExtension[] { + return [...this._extensions]; + } + + get cmExtensions(): CMExtension[] { + return [...this._cmExtensions]; + } + + get resolvedExtensions(): InkwellExtension[] { + return [...this._resolvedExtensions]; + } + + /** The resolved extension array, including child extensions */ + resolveExtensions(): InkwellExtension[] { + const resolved: InkwellExtension[] = []; + const visited = new Set(); + const visitedNames = new Set(); + + // TODO: add name-guarding / deduping + const resolve = (ext: InkwellExtension) => { + if (visited.has(ext) || visitedNames.has(ext.name)) return; + + visited.add(ext); + resolved.push(ext); + visitedNames.add(ext.name); + + if (ext.addExtensions) { + const childExtensions = ext.addExtensions({ editor: this.editor }); + for (const childExtension of childExtensions) { + resolve(childExtension); + } + } + }; + + for (const ext of this._extensions) { + resolve(ext); + } + + return resolved; + } + + public sortExtensionsByPrio(extensions: InkwellExtension[]): InkwellExtension[] { + return [...extensions].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); + } + + public toCodeMirrorExtensions(): CMExtension[] { + const cmExtensions: CMExtension[] = []; + + for (const ext of this.sortExtensionsByPrio(this.resolvedExtensions)) { + if (ext.addCodeMirrorExtensions) { + const cmExts = ext.addCodeMirrorExtensions({ editor: this.editor }); + cmExtensions.push(...cmExts); + } + } + + return cmExtensions; + } +} diff --git a/packages/core/src/extensions/index.ts b/packages/core/src/extensions/index.ts new file mode 100644 index 0000000..7e45f11 --- /dev/null +++ b/packages/core/src/extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./types.ts"; +export * from "./ExtensionManager.ts"; diff --git a/packages/core/src/extensions/types.ts b/packages/core/src/extensions/types.ts new file mode 100644 index 0000000..46ef2c5 --- /dev/null +++ b/packages/core/src/extensions/types.ts @@ -0,0 +1,39 @@ +import type { Editor } from "../editor/index.ts"; +import type { Extension as CMExtension } from "@codemirror/state"; +import type { EditorEvents } from "../editor/types.ts"; + +export interface EditorContext { + editor: Editor; +} + +// TODO: define the types for the extensions +export type NodeConfig = { + name: string; +}; + +// TODO: define the types for the extensions +export type MarkConfig = { + name: string; +}; + +export type ExtensionEventHandlers = { + [K in keyof EditorEvents]?: (ctx: EditorContext, payload: EditorEvents[K]) => void; +}; + +export type InkwellExtension = ExtensionEventHandlers & { + name: string; + + /** + * The extension priority. Extensions with higher priority will be initialized first. Defaults to 0. + * @example 10 + */ + priority?: number; + + addNodes?: (ctx: EditorContext) => NodeConfig[]; + addMarks?: (ctx: EditorContext) => MarkConfig[]; + addCodeMirrorExtensions?: (ctx: EditorContext) => CMExtension[]; + addExtensions?: (ctx: EditorContext) => InkwellExtension[]; + + // TODO: define addKeybinds correctly + addKeybinds?: (ctx: EditorContext) => Record void>; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95ae7c4..678f804 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,12 @@ importers: apps/playground: dependencies: + '@codemirror/state': + specifier: 'catalog:' + version: 6.7.1 + '@codemirror/view': + specifier: 'catalog:' + version: 6.43.7 astro: specifier: ^7.1.6 version: 7.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@26.1.2)(jiti@2.7.0)(typescript@7.0.2)(yaml@2.9.0)