@llui/markdown-editor 0.2.12 → 0.2.13

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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @llui/markdown-editor
2
+
3
+ WYSIWYG Markdown editor for [LLui](https://github.com/fponticelli/llui) — hides Markdown behind a rich, pluggable editing widget built on Lexical.
4
+
5
+ ```bash
6
+ pnpm add @llui/markdown-editor @llui/lexical @llui/components lexical
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```ts
12
+ import { mountApp } from '@llui/dom'
13
+ import { markdownEditor } from '@llui/markdown-editor'
14
+ import '@llui/markdown-editor/styles/editor.css'
15
+
16
+ // `markdownEditor()` returns a component definition; mount it (or compose it).
17
+ mountApp(
18
+ document.getElementById('editor')!,
19
+ markdownEditor({
20
+ defaultValue: '# Hello\n\nStart typing…',
21
+ toolbar: true,
22
+ onChange: (markdown) => console.log(markdown),
23
+ }),
24
+ )
25
+ ```
26
+
27
+ ## What it provides
28
+
29
+ - **`markdownEditor()`** — the editor component, built on the [`@llui/lexical`](https://www.npmjs.com/package/@llui/lexical) seam.
30
+ - **Transformer registry** — GFM and callout plugins (`./plugins/core`, `./plugins/callout`) that map Markdown constructs to Lexical nodes.
31
+ - **Toolbar surface** — an optional formatting toolbar (`./surfaces/toolbar`).
32
+ - **`collab` seam** — opt-in collaborative editing via [`@llui/lexical-collab`](https://www.npmjs.com/package/@llui/lexical-collab).
33
+
34
+ ## Entry points
35
+
36
+ | Import | Purpose |
37
+ | ----------------------------------------- | ------------------------------ |
38
+ | `@llui/markdown-editor` | `markdownEditor()` component |
39
+ | `@llui/markdown-editor/plugins/core` | Core GFM transformers |
40
+ | `@llui/markdown-editor/plugins/callout` | Callout/admonition transformer |
41
+ | `@llui/markdown-editor/surfaces/toolbar` | Toolbar surface |
42
+ | `@llui/markdown-editor/styles/editor.css` | Editor styles |
43
+
44
+ Peers on `@llui/dom`, `@llui/lexical`, `@llui/components`, `lexical`, and the relevant `@lexical/*` packages (`^0.46`).
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@llui/markdown-editor",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
11
  },
12
12
  "./plugins/core": {
13
- "import": "./dist/plugins/core.js",
14
- "types": "./dist/plugins/core.d.ts"
13
+ "types": "./dist/plugins/core.d.ts",
14
+ "import": "./dist/plugins/core.js"
15
15
  },
16
16
  "./plugins/callout": {
17
- "import": "./dist/plugins/callout.js",
18
- "types": "./dist/plugins/callout.d.ts"
17
+ "types": "./dist/plugins/callout.d.ts",
18
+ "import": "./dist/plugins/callout.js"
19
19
  },
20
20
  "./surfaces/toolbar": {
21
- "import": "./dist/surfaces/toolbar.js",
22
- "types": "./dist/surfaces/toolbar.d.ts"
21
+ "types": "./dist/surfaces/toolbar.d.ts",
22
+ "import": "./dist/surfaces/toolbar.js"
23
23
  },
24
24
  "./styles/editor.css": "./dist/styles/editor.css"
25
25
  },
@@ -50,9 +50,9 @@
50
50
  "typescript": "^6.0.0",
51
51
  "vitest": "^4.1.2",
52
52
  "@lexical/code-core": "^0.46.0",
53
- "@llui/dom": "0.11.5",
54
- "@llui/lexical": "0.2.8",
55
- "@llui/components": "0.12.2"
53
+ "@llui/dom": "0.11.7",
54
+ "@llui/lexical": "0.2.9",
55
+ "@llui/components": "0.12.3"
56
56
  },
57
57
  "sideEffects": [
58
58
  "./dist/styles/editor.css"
@@ -78,8 +78,12 @@
78
78
  },
79
79
  "homepage": "https://github.com/fponticelli/llui/tree/main/packages/markdown-editor#readme",
80
80
  "files": [
81
- "dist"
81
+ "dist",
82
+ "src"
82
83
  ],
84
+ "publishConfig": {
85
+ "access": "public"
86
+ },
83
87
  "scripts": {
84
88
  "build": "tsc -p tsconfig.build.json && mkdir -p dist/styles && cp src/styles/editor.css dist/styles/",
85
89
  "check": "tsc --noEmit -p tsconfig.check.json",
package/src/editor.ts ADDED
@@ -0,0 +1,303 @@
1
+ // `markdownEditor(config)` — the high-level component. Lexical owns the live
2
+ // document; this wires the foreign seam to the markdown transformer converters,
3
+ // surfaces the format state for the chrome, routes command intents back to the
4
+ // live editor through effects, and COMPOSES plugin UI extensions (each plugin's
5
+ // state slice + reducer + view + effects) into the single component.
6
+
7
+ import { $getRoot, $setSelection, type EditorThemeClasses, type LexicalEditor } from 'lexical'
8
+ import {
9
+ $convertFromMarkdownString,
10
+ $convertToMarkdownString,
11
+ registerMarkdownShortcuts,
12
+ } from '@lexical/markdown'
13
+ import { component, div, type Renderable, type Signal, type SignalComponentDef } from '@llui/dom'
14
+ import {
15
+ lexicalForeign,
16
+ registerDecoratorBridges,
17
+ PROGRAMMATIC_TAG,
18
+ type DecoratorBridge,
19
+ } from '@llui/lexical'
20
+ import { corePlugin } from './plugins/core.js'
21
+ import { linkPlugin } from './plugins/link.js'
22
+ import { registerMarkdownPaste } from './paste.js'
23
+ import { toolbar as renderToolbar } from './surfaces/toolbar.js'
24
+ import type { CommandItem, MarkdownPlugin } from './plugins/types.js'
25
+ import type { PluginUI } from './plugins/ui.js'
26
+ import { buildTransformers } from './transformers/registry.js'
27
+ import { mergeTheme } from './theme.js'
28
+ import { computeFormatState } from './format.js'
29
+ import { makeOnEffect } from './effects.js'
30
+ import {
31
+ countWords,
32
+ init,
33
+ update,
34
+ type EditorEffect,
35
+ type EditorMsg,
36
+ type EditorOutMsg,
37
+ type EditorState,
38
+ type FormatState,
39
+ } from './state.js'
40
+
41
+ export interface EditorConfig {
42
+ /** Plugins composing the feature set; order defines transformer precedence.
43
+ * Defaults to `[corePlugin(), linkPlugin()]` so the minimal editor has GFM + links. */
44
+ plugins?: readonly MarkdownPlugin[]
45
+ /** Initial markdown (uncontrolled seed). */
46
+ defaultValue?: string
47
+ /** Controlled: the consumer owns this signal; the editor follows it. */
48
+ value?: Signal<string>
49
+ /** Debounced markdown-emission window (ms). Default 300. */
50
+ changeDebounceMs?: number
51
+ placeholder?: string
52
+ readonly?: boolean
53
+ /** Lexical theme class map. */
54
+ theme?: EditorThemeClasses
55
+ /** Editor namespace (instance isolation). */
56
+ namespace?: string
57
+ /** Outbound markdown (after debounce). */
58
+ onChange?: (markdown: string) => void
59
+ /** Outbound format surface (for chrome built outside this package). */
60
+ onFormatChange?: (format: FormatState) => void
61
+ /** Receives the live Lexical editor at mount (imperative access, collab hooks). */
62
+ onReady?: (editor: LexicalEditor) => void
63
+ /** Render the built-in toolbar above the editor. Default false (minimal). */
64
+ toolbar?: boolean
65
+ /** Convert plain-text Markdown to rich content on paste. Default true.
66
+ * Pastes that carry `text/html` are always left to Lexical's HTML import,
67
+ * regardless of this flag. Set false to paste Markdown as literal text. */
68
+ pasteMarkdown?: boolean
69
+ /** Enable collaborative editing. The editor hands you a markdown `seed` and
70
+ * status sinks; return a binding (build it with `yjsCollab` from
71
+ * `@llui/lexical-collab`, wiring your own provider). Mutually exclusive with
72
+ * `value` — the shared CRDT document, not a markdown signal, owns the content.
73
+ * `defaultValue` becomes the seed the bootstrapping peer writes. */
74
+ collab?: CollabFactory
75
+ }
76
+
77
+ /** Disposer-returning binding the collab layer installs on the live editor.
78
+ * `@llui/lexical-collab`'s `YjsCollab` satisfies this structurally, so
79
+ * `@llui/markdown-editor` needs no Yjs dependency of its own. */
80
+ export interface CollabBinding {
81
+ register: (editor: LexicalEditor) => () => void
82
+ }
83
+
84
+ /** Hooks the editor injects into the {@link CollabFactory}: a markdown `seed`
85
+ * (run once by the bootstrapping peer to fill an empty shared doc from
86
+ * `defaultValue`) plus status sinks the editor mirrors into `state.collab`.
87
+ * Spread straight into `yjsCollab({ id, provider, user, ...hooks })`. */
88
+ export interface CollabHooks {
89
+ seed: (editor: LexicalEditor) => void
90
+ onStatus: (connected: boolean) => void
91
+ onSync: (synced: boolean) => void
92
+ onPeers: (count: number) => void
93
+ }
94
+
95
+ /** Builds the collab binding from the editor-supplied hooks. */
96
+ export type CollabFactory = (hooks: CollabHooks) => CollabBinding
97
+
98
+ /** Hooks the chrome layer (toolbar/menus) uses to compose around the editor. */
99
+ export interface EditorParts {
100
+ /** The merged, surface-filtered command items. */
101
+ items: readonly CommandItem[]
102
+ /** Reactive format signal for `connect`-style toolbars. */
103
+ format: Signal<FormatState>
104
+ }
105
+
106
+ /** Default plugin set when the consumer supplies none. */
107
+ function defaultPlugins(): MarkdownPlugin[] {
108
+ return [corePlugin(), linkPlugin()]
109
+ }
110
+
111
+ /**
112
+ * Build the markdown editor component. Embed it with `mountApp(el, markdownEditor(...))`
113
+ * or compose it inside a larger component.
114
+ */
115
+ export function markdownEditor(
116
+ config: EditorConfig = {},
117
+ ): SignalComponentDef<EditorState, EditorMsg, EditorEffect> {
118
+ const plugins = config.plugins && config.plugins.length > 0 ? config.plugins : defaultPlugins()
119
+ const transformers = buildTransformers(plugins)
120
+
121
+ const items: CommandItem[] = plugins.flatMap((p) => p.items ?? [])
122
+ const itemsById = new Map(items.map((i) => [i.id, i]))
123
+ // Share the merged item list with plugins that want it (e.g. the slash menu).
124
+ for (const plugin of plugins) plugin.onItems?.(items)
125
+ const decorators: DecoratorBridge[] = plugins.flatMap((p) => p.decorators ?? [])
126
+ const pluginUIs: Array<{ name: string; ui: PluginUI }> = plugins
127
+ .filter((p): p is MarkdownPlugin & { ui: PluginUI } => p.ui !== undefined)
128
+ .map((p) => ({ name: p.name, ui: p.ui }))
129
+ const pluginUIByName = new Map(pluginUIs.map((p) => [p.name, p.ui]))
130
+
131
+ // The live editor, captured at mount; effects dispatch through it.
132
+ let editorRef: LexicalEditor | null = null
133
+ const getEditor = (): LexicalEditor | null => editorRef
134
+
135
+ const baseOnEffect = makeOnEffect(getEditor, itemsById, {
136
+ onChange: config.onChange,
137
+ onFormatChange: config.onFormatChange,
138
+ applyValue: (editor, value) =>
139
+ editor.update(
140
+ () => {
141
+ $convertFromMarkdownString(value, transformers)
142
+ // Clear selection so the reconciler doesn't pull DOM focus into the
143
+ // editor on an external push (e.g. typing in a bound source textarea).
144
+ $setSelection(null)
145
+ },
146
+ { tag: PROGRAMMATIC_TAG },
147
+ ),
148
+ })
149
+
150
+ if (config.collab && config.value) {
151
+ throw new Error(
152
+ 'markdownEditor: `collab` and `value` are mutually exclusive — in a collaborative ' +
153
+ 'session the shared CRDT document owns the content, not a markdown signal. ' +
154
+ 'Use `defaultValue` as the bootstrap seed instead.',
155
+ )
156
+ }
157
+
158
+ const collabEnabled = !!config.collab
159
+ const seedValue = config.value ? config.value.peek() : (config.defaultValue ?? '')
160
+
161
+ // ── Composed TEA: core + plugin UI slices ──────────────────────────────────
162
+ const composedInit = (): [EditorState, EditorEffect[]] => {
163
+ const [core, effects] = init({
164
+ value: seedValue,
165
+ readonly: config.readonly ?? false,
166
+ collab: collabEnabled,
167
+ })
168
+ const slices: Record<string, unknown> = {}
169
+ for (const { name, ui } of pluginUIs) slices[name] = ui.init()
170
+ return [{ ...core, plugins: slices }, effects]
171
+ }
172
+
173
+ const composedUpdate = (state: EditorState, msg: EditorMsg): [EditorState, EditorEffect[]] => {
174
+ if (msg.type === 'plugin') {
175
+ const ui = pluginUIByName.get(msg.name)
176
+ if (!ui?.update) return [state, []]
177
+ const result = ui.update(state.plugins[msg.name], msg.msg)
178
+ const [slice, effects] = (Array.isArray(result) ? result : [result, []]) as [
179
+ unknown,
180
+ unknown[],
181
+ ]
182
+ return [
183
+ { ...state, plugins: { ...state.plugins, [msg.name]: slice } },
184
+ effects.map((effect) => ({ type: 'pluginEffect' as const, name: msg.name, effect })),
185
+ ]
186
+ }
187
+ return update(state, msg)
188
+ }
189
+
190
+ const composedOnEffect = (
191
+ effect: EditorEffect,
192
+ api: { send: (msg: EditorMsg) => void; state: Signal<EditorState> },
193
+ ): void => {
194
+ if (effect.type === 'pluginEffect') {
195
+ const ui = pluginUIByName.get(effect.name)
196
+ ui?.onEffect?.(effect.effect, {
197
+ editor: getEditor,
198
+ send: (msg) => api.send({ type: 'plugin', name: effect.name, msg }),
199
+ emit: (msg) => api.send(msg as EditorMsg),
200
+ })
201
+ return
202
+ }
203
+ baseOnEffect(effect, api)
204
+ }
205
+
206
+ const view = ({
207
+ state,
208
+ send,
209
+ }: {
210
+ state: Signal<EditorState>
211
+ send: (msg: EditorMsg) => void
212
+ }): Renderable => {
213
+ // Build the collab binding (once, at mount) from the consumer's factory,
214
+ // injecting the markdown seed + status sinks that mirror into `state.collab`.
215
+ const collabBinding: CollabBinding | null = config.collab
216
+ ? config.collab({
217
+ seed: () => {
218
+ $convertFromMarkdownString(seedValue, transformers)
219
+ $setSelection(null)
220
+ },
221
+ onStatus: (connected) => send({ type: 'collabStatus', connected }),
222
+ onSync: (synced) => send({ type: 'collabSync', synced }),
223
+ onPeers: (peers) => send({ type: 'collabPeers', peers }),
224
+ })
225
+ : null
226
+
227
+ const host = lexicalForeign<EditorOutMsg>({
228
+ namespace: config.namespace ?? 'llui-markdown',
229
+ theme: mergeTheme(config.theme),
230
+ plugins,
231
+ serialize: (editor) =>
232
+ editor.getEditorState().read(() => $convertToMarkdownString(transformers)),
233
+ deserialize: (_editor, value) => {
234
+ $convertFromMarkdownString(value, transformers)
235
+ $setSelection(null)
236
+ },
237
+ // In collab mode the shared CRDT owns the document: the local undo stack
238
+ // and the boot-time seed are disabled — the binding supplies a scoped undo
239
+ // manager and a sync-gated bootstrap instead.
240
+ ...(collabBinding ? { history: false, seedMode: 'deferred' as const } : {}),
241
+ defaultValue: collabBinding || config.value ? undefined : (config.defaultValue ?? ''),
242
+ ...(config.value && !collabBinding ? { value: config.value } : {}),
243
+ readonly: state.at('readonly'),
244
+ ...(config.changeDebounceMs !== undefined
245
+ ? { changeDebounceMs: config.changeDebounceMs }
246
+ : {}),
247
+ register: (editor) => {
248
+ const disposers = [registerMarkdownShortcuts(editor, transformers)]
249
+ if (config.pasteMarkdown !== false)
250
+ disposers.push(registerMarkdownPaste(editor, transformers))
251
+ if (decorators.length > 0) disposers.push(registerDecoratorBridges(editor, decorators))
252
+ if (collabBinding) disposers.push(collabBinding.register(editor))
253
+ return () => {
254
+ for (const dispose of disposers) dispose()
255
+ }
256
+ },
257
+ onReady: (editor) => {
258
+ editorRef = editor
259
+ if (config.placeholder) {
260
+ editor.getRootElement()?.setAttribute('data-placeholder', config.placeholder)
261
+ }
262
+ config.onReady?.(editor)
263
+ },
264
+ onChange: (value) => send({ type: 'markdownChanged', value }),
265
+ onSelectionChange: (ctx) => {
266
+ const format = computeFormatState(ctx.editor, ctx)
267
+ const text = ctx.editor.getEditorState().read(() => $getRoot().getTextContent())
268
+ // Toggle an empty marker so CSS can show the placeholder.
269
+ ctx.editor.getRootElement()?.setAttribute('data-empty', text === '' ? 'true' : 'false')
270
+ send({ type: 'formatChanged', format, wordCount: countWords(text), charCount: text.length })
271
+ },
272
+ emit: (msg) => send(msg),
273
+ })
274
+
275
+ // Plugin view contributions (overlays/panels) — each gets its own slice + send.
276
+ const pluginViews: Renderable = pluginUIs.flatMap(({ name, ui }) => {
277
+ if (!ui.view) return []
278
+ const rendered = ui.view({
279
+ state: state.at(`plugins.${name}`),
280
+ send: (msg) => send({ type: 'plugin', name, msg }),
281
+ editor: getEditor,
282
+ })
283
+ return Array.isArray(rendered) ? rendered : [rendered]
284
+ })
285
+
286
+ if (!config.toolbar) return [host, ...pluginViews]
287
+ return [
288
+ div({ 'data-scope': 'md-editor', 'data-part': 'root' }, [
289
+ renderToolbar({ format: state.at('format'), send, items, collab: state.at('collab') }),
290
+ div({ 'data-scope': 'md-editor', 'data-part': 'surface' }, [host]),
291
+ ]),
292
+ ...pluginViews,
293
+ ]
294
+ }
295
+
296
+ return component<EditorState, EditorMsg, EditorEffect>({
297
+ name: 'MarkdownEditor',
298
+ init: composedInit,
299
+ update: composedUpdate,
300
+ view,
301
+ onEffect: composedOnEffect,
302
+ })
303
+ }
package/src/effects.ts ADDED
@@ -0,0 +1,51 @@
1
+ // Effect handler: the only place TEA reaches back into the live Lexical editor.
2
+ // `execCommand` looks an id up in the merged command-item map and runs it on the
3
+ // editor captured at mount; emit* forward to the consumer's callbacks.
4
+
5
+ import type { LexicalEditor } from 'lexical'
6
+ import type { CommandItem } from './plugins/types.js'
7
+ import type { EditorEffect, EditorMsg, FormatState } from './state.js'
8
+
9
+ /** The api the component passes to `onEffect` (send + state). */
10
+ export interface EffectApi {
11
+ send: (msg: EditorMsg) => void
12
+ }
13
+
14
+ export interface EffectConfig {
15
+ onChange?: (markdown: string) => void
16
+ onFormatChange?: (format: FormatState) => void
17
+ /** Push markdown into the live editor (deserialize), without echoing onChange. */
18
+ applyValue: (editor: LexicalEditor, value: string) => void
19
+ }
20
+
21
+ /** Build the component's `onEffect`. `getEditor` returns the live editor (set at
22
+ * mount via the foreign `onReady`); `items` is the merged id → command map. */
23
+ export function makeOnEffect(
24
+ getEditor: () => LexicalEditor | null,
25
+ items: ReadonlyMap<string, CommandItem>,
26
+ config: EffectConfig,
27
+ ): (effect: EditorEffect, api: EffectApi) => void {
28
+ return (effect, api) => {
29
+ switch (effect.type) {
30
+ case 'execCommand': {
31
+ const editor = getEditor()
32
+ const item = items.get(effect.id)
33
+ if (editor && item) item.run(editor, { send: api.send })
34
+ return
35
+ }
36
+ case 'applyValue': {
37
+ const editor = getEditor()
38
+ if (editor) config.applyValue(editor, effect.value)
39
+ return
40
+ }
41
+ case 'emitChange': {
42
+ config.onChange?.(effect.value)
43
+ return
44
+ }
45
+ case 'emitFormat': {
46
+ config.onFormatChange?.(effect.format)
47
+ return
48
+ }
49
+ }
50
+ }
51
+ }
package/src/format.ts ADDED
@@ -0,0 +1,57 @@
1
+ // Compute the full toolbar FormatState from a Lexical selection — the base
2
+ // rich-text format (from @llui/lexical) refined with list/code/link detection
3
+ // (which needs the list/code/link packages this layer depends on).
4
+
5
+ import { $getSelection, $isRangeSelection, type LexicalEditor } from 'lexical'
6
+ import { $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
7
+ import { ListNode, $isListItemNode } from '@lexical/list'
8
+ import { $isCodeNode } from '@lexical/code-core'
9
+ import { $isLinkNode } from '@lexical/link'
10
+ import { $readBaseFormat } from '@llui/lexical'
11
+ import type { SelectionContext } from '@llui/lexical'
12
+ import { EMPTY_FORMAT, type BlockType, type FormatState } from './state.js'
13
+
14
+ /** Read the full format surface at the current selection (opens a read ctx). */
15
+ export function computeFormatState(
16
+ editor: LexicalEditor,
17
+ history: Pick<SelectionContext, 'canUndo' | 'canRedo'>,
18
+ ): FormatState {
19
+ return editor.getEditorState().read(() => {
20
+ const base = $readBaseFormat()
21
+ if (!base.hasSelection) {
22
+ return { ...EMPTY_FORMAT, canUndo: history.canUndo, canRedo: history.canRedo }
23
+ }
24
+
25
+ let blockType: BlockType = base.blockType
26
+ let link = false
27
+
28
+ const selection = $getSelection()
29
+ if ($isRangeSelection(selection)) {
30
+ const anchorNode = selection.anchor.getNode()
31
+ link = $findMatchingParent(anchorNode, (node) => $isLinkNode(node)) !== null
32
+
33
+ if (base.blockType === 'other') {
34
+ const listItem = $findMatchingParent(anchorNode, (node) => $isListItemNode(node))
35
+ if (listItem) {
36
+ const list = $getNearestNodeOfType(anchorNode, ListNode)
37
+ if (list) blockType = list.getListType()
38
+ } else {
39
+ const top = anchorNode.getKey() === 'root' ? null : anchorNode.getTopLevelElement()
40
+ if (top && $isCodeNode(top)) blockType = 'code'
41
+ }
42
+ }
43
+ }
44
+
45
+ return {
46
+ bold: base.bold,
47
+ italic: base.italic,
48
+ strikethrough: base.strikethrough,
49
+ code: base.code,
50
+ link,
51
+ blockType,
52
+ alignment: base.alignment,
53
+ canUndo: history.canUndo,
54
+ canRedo: history.canRedo,
55
+ }
56
+ })
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,93 @@
1
+ // `@llui/markdown-editor` — WYSIWYG Markdown editor for LLui, built on Lexical.
2
+
3
+ export {
4
+ type EditorConfig,
5
+ type EditorParts,
6
+ type CollabBinding,
7
+ type CollabHooks,
8
+ type CollabFactory,
9
+ markdownEditor,
10
+ } from './editor.js'
11
+
12
+ export {
13
+ type BlockType,
14
+ type FormatState,
15
+ type OverlayKind,
16
+ type CollabStatus,
17
+ type EditorState,
18
+ type EditorMsg,
19
+ type EditorOutMsg,
20
+ type EditorEffect,
21
+ type InitOptions,
22
+ EMPTY_FORMAT,
23
+ COLLAB_OFF,
24
+ init,
25
+ update,
26
+ countWords,
27
+ } from './state.js'
28
+
29
+ export {
30
+ type ItemSurface,
31
+ type CommandItem,
32
+ type CommandContext,
33
+ type MarkdownPlugin,
34
+ } from './plugins/types.js'
35
+
36
+ export {
37
+ type PluginUI,
38
+ type PluginUISpec,
39
+ type PluginViewArgs,
40
+ type PluginEffectContext,
41
+ definePluginUI,
42
+ } from './plugins/ui.js'
43
+
44
+ export { type CorePluginOptions, corePlugin } from './plugins/core.js'
45
+ export {
46
+ type InlineFormat,
47
+ type SingleBlockPluginOptions,
48
+ singleBlockPlugin,
49
+ } from './plugins/single-block.js'
50
+ export { type LinkPluginOptions, linkPlugin } from './plugins/link.js'
51
+ export {
52
+ type CalloutKind,
53
+ type CalloutData,
54
+ type CalloutPluginOptions,
55
+ calloutPlugin,
56
+ $insertCallout,
57
+ } from './plugins/callout.js'
58
+ export { hrPlugin, $insertHorizontalRule } from './plugins/hr.js'
59
+ export { slashPlugin } from './plugins/slash.js'
60
+ export { contextMenuPlugin } from './plugins/context-menu.js'
61
+ export { floatingToolbarPlugin } from './plugins/floating-toolbar.js'
62
+ export { type MathPluginOptions, mathPlugin } from './plugins/math.js'
63
+ export { type MermaidPluginOptions, mermaidPlugin } from './plugins/mermaid.js'
64
+ export { type Mention, type MentionPluginOptions, mentionPlugin } from './plugins/mention.js'
65
+ export { type EmojiPluginOptions, DEFAULT_EMOJI, emojiPlugin } from './plugins/emoji.js'
66
+ export { type ImagePluginOptions, imagePlugin } from './plugins/image.js'
67
+ export { tablePlugin } from './plugins/table.js'
68
+
69
+ export { $insertMarkdownAtSelection, registerMarkdownPaste } from './paste.js'
70
+
71
+ export { GFM_NODES, GFM_TRANSFORMERS } from './transformers/gfm.js'
72
+ export { buildTransformers, orderTransformers } from './transformers/registry.js'
73
+
74
+ export { computeFormatState } from './format.js'
75
+
76
+ export {
77
+ STRIKETHROUGH_CLASS,
78
+ UNDERLINE_CLASS,
79
+ UNDERLINE_STRIKETHROUGH_CLASS,
80
+ defaultTheme,
81
+ mergeTheme,
82
+ } from './theme.js'
83
+
84
+ export {
85
+ type ToolbarItemParts,
86
+ type ToolbarParts,
87
+ type ToolbarOptions,
88
+ DEFAULT_GLYPHS,
89
+ connectToolbar,
90
+ toolbar,
91
+ } from './surfaces/toolbar.js'
92
+
93
+ export { type LinkDialogOptions, linkDialog } from './surfaces/link-dialog.js'