@llui/markdown-editor 0.2.14 → 0.4.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.
- package/dist/editor.d.ts +22 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +45 -6
- package/dist/editor.js.map +1 -1
- package/dist/effects.d.ts +0 -1
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +4 -1
- package/dist/effects.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/plugins/block-drag.d.ts +73 -0
- package/dist/plugins/block-drag.d.ts.map +1 -0
- package/dist/plugins/block-drag.js +760 -0
- package/dist/plugins/block-drag.js.map +1 -0
- package/dist/plugins/code-language.d.ts +60 -0
- package/dist/plugins/code-language.d.ts.map +1 -0
- package/dist/plugins/code-language.js +308 -0
- package/dist/plugins/code-language.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +45 -0
- package/dist/plugins/frontmatter.d.ts.map +1 -0
- package/dist/plugins/frontmatter.js +281 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/overlay.d.ts +1 -0
- package/dist/plugins/overlay.d.ts.map +1 -1
- package/dist/plugins/overlay.js +7 -0
- package/dist/plugins/overlay.js.map +1 -1
- package/dist/plugins/table.d.ts.map +1 -1
- package/dist/plugins/table.js +77 -15
- package/dist/plugins/table.js.map +1 -1
- package/dist/plugins/wikilink.d.ts +91 -0
- package/dist/plugins/wikilink.d.ts.map +1 -0
- package/dist/plugins/wikilink.js +467 -0
- package/dist/plugins/wikilink.js.map +1 -0
- package/dist/styles/block-drag.css +63 -0
- package/dist/styles/editor.css +64 -6
- package/dist/theme.d.ts +2 -5
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +15 -11
- package/dist/theme.js.map +1 -1
- package/dist/transformers/code.d.ts +24 -0
- package/dist/transformers/code.d.ts.map +1 -0
- package/dist/transformers/code.js +167 -0
- package/dist/transformers/code.js.map +1 -0
- package/dist/transformers/gfm.d.ts +9 -1
- package/dist/transformers/gfm.d.ts.map +1 -1
- package/dist/transformers/gfm.js +16 -4
- package/dist/transformers/gfm.js.map +1 -1
- package/dist/transformers/registry.d.ts +3 -0
- package/dist/transformers/registry.d.ts.map +1 -1
- package/dist/transformers/registry.js +26 -0
- package/dist/transformers/registry.js.map +1 -1
- package/package.json +45 -31
- package/src/editor.ts +77 -9
- package/src/effects.ts +4 -2
- package/src/index.ts +61 -9
- package/src/plugins/block-drag.ts +912 -0
- package/src/plugins/code-language.ts +378 -0
- package/src/plugins/frontmatter.ts +324 -0
- package/src/plugins/overlay.ts +7 -0
- package/src/plugins/table.ts +87 -13
- package/src/plugins/wikilink.ts +553 -0
- package/src/styles/block-drag.css +63 -0
- package/src/styles/editor.css +64 -6
- package/src/theme.ts +15 -11
- package/src/transformers/code.ts +174 -0
- package/src/transformers/gfm.ts +16 -4
- package/src/transformers/registry.ts +27 -0
- package/dist/__llui_deps.json +0 -274
package/src/editor.ts
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
// live editor through effects, and COMPOSES plugin UI extensions (each plugin's
|
|
5
5
|
// state slice + reducer + view + effects) into the single component.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
$getRoot,
|
|
9
|
+
$setSelection,
|
|
10
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
11
|
+
FORMAT_TEXT_COMMAND,
|
|
12
|
+
type EditorThemeClasses,
|
|
13
|
+
type LexicalEditor,
|
|
14
|
+
} from 'lexical'
|
|
8
15
|
import {
|
|
9
16
|
$convertFromMarkdownString,
|
|
10
17
|
$convertToMarkdownString,
|
|
@@ -76,10 +83,23 @@ export interface EditorConfig {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/** Disposer-returning binding the collab layer installs on the live editor.
|
|
79
|
-
* `@llui/lexical-collab`'s `YjsCollab`
|
|
80
|
-
* `@llui/markdown-editor` needs
|
|
86
|
+
* `@llui/lexical-collab`'s `YjsCollab` and `@llui/lexical-loro`'s `LoroCollab`
|
|
87
|
+
* both satisfy this structurally, so `@llui/markdown-editor` needs neither a Yjs
|
|
88
|
+
* nor a Loro dependency of its own. */
|
|
81
89
|
export interface CollabBinding {
|
|
82
90
|
register: (editor: LexicalEditor) => () => void
|
|
91
|
+
/**
|
|
92
|
+
* A CRDT-aware undo owner, if the binding provides one SEPARATELY from
|
|
93
|
+
* `register`. When present it is handed to `lexicalForeign({ externalUndo })`,
|
|
94
|
+
* which forces the built-in `@lexical/history` stack off so the two can never
|
|
95
|
+
* both be live — this is what gives collab mode real, peer-scoped undo.
|
|
96
|
+
*
|
|
97
|
+
* Optional because not every binding splits undo out this way: `yjsCollab`
|
|
98
|
+
* installs its own undo commands INSIDE `register`, so it leaves this unset and
|
|
99
|
+
* still owns undo. A binding that sets neither would leave the editor with no
|
|
100
|
+
* undo at all — see `@llui/lexical-loro`, which sets this.
|
|
101
|
+
*/
|
|
102
|
+
externalUndo?: (editor: LexicalEditor) => () => void
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
/** Hooks the editor injects into the {@link CollabFactory}: a markdown `seed`
|
|
@@ -109,13 +129,31 @@ function defaultPlugins(): MarkdownPlugin[] {
|
|
|
109
129
|
return [corePlugin(), linkPlugin()]
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
/** Swallow the underline text-format command. `registerRichText` wires Cmd+U to
|
|
133
|
+
* FORMAT_TEXT 'underline', but the GFM markdown dialect this editor serializes
|
|
134
|
+
* has no underline representation, so an applied underline would be silently
|
|
135
|
+
* stripped on save. Intercepting at CRITICAL priority (ahead of rich-text) keeps
|
|
136
|
+
* the WYSIWYG surface and the serialized dialect in lock-step: underline can be
|
|
137
|
+
* neither applied nor lost. Returns a disposer. */
|
|
138
|
+
export function blockUnderlineFormat(editor: LexicalEditor): () => void {
|
|
139
|
+
return editor.registerCommand(
|
|
140
|
+
FORMAT_TEXT_COMMAND,
|
|
141
|
+
(payload) => payload === 'underline',
|
|
142
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/** The per-mount `send` closure — the WeakMap key that ties a live editor to the
|
|
113
147
|
* one mount it belongs to. */
|
|
114
148
|
type EditorSend = (msg: EditorMsg) => void
|
|
115
149
|
|
|
116
|
-
/** Per-mount live state: the Lexical editor captured by that mount's `onReady
|
|
150
|
+
/** Per-mount live state: the Lexical editor captured by that mount's `onReady`,
|
|
151
|
+
* plus the last markdown delivered to the consumer's `onChange` (dedup key). */
|
|
117
152
|
interface MountContext {
|
|
118
153
|
editor: LexicalEditor | null
|
|
154
|
+
/** Last value handed to `config.onChange` for this mount — guards against
|
|
155
|
+
* double / redundant consumer notifications. `undefined` = nothing emitted yet. */
|
|
156
|
+
lastChange: string | undefined
|
|
119
157
|
}
|
|
120
158
|
|
|
121
159
|
/**
|
|
@@ -148,14 +186,17 @@ export function markdownEditor(
|
|
|
148
186
|
const contextFor = (send: EditorSend): MountContext => {
|
|
149
187
|
let ctx = mountContexts.get(send)
|
|
150
188
|
if (!ctx) {
|
|
151
|
-
ctx = { editor: null }
|
|
189
|
+
ctx = { editor: null, lastChange: undefined }
|
|
152
190
|
mountContexts.set(send, ctx)
|
|
153
191
|
}
|
|
154
192
|
return ctx
|
|
155
193
|
}
|
|
156
194
|
|
|
157
195
|
const baseOnEffect = makeOnEffect((api) => contextFor(api.send).editor, itemsById, {
|
|
158
|
-
|
|
196
|
+
// NB: consumer `onChange` is delivered DIRECTLY from the foreign onChange
|
|
197
|
+
// wrapper (see `view` below), NOT through this effect path — the dispose-time
|
|
198
|
+
// debounce flush runs after the TEA loop is disposed, and a `send` from a
|
|
199
|
+
// disposed loop is dropped, so effect-routed delivery loses the final edit.
|
|
159
200
|
onFormatChange: config.onFormatChange,
|
|
160
201
|
applyValue: (editor, value) =>
|
|
161
202
|
editor.update(
|
|
@@ -263,8 +304,18 @@ export function markdownEditor(
|
|
|
263
304
|
},
|
|
264
305
|
// In collab mode the shared CRDT owns the document: the local undo stack
|
|
265
306
|
// and the boot-time seed are disabled — the binding supplies a scoped undo
|
|
266
|
-
// manager and a sync-gated bootstrap instead.
|
|
267
|
-
|
|
307
|
+
// manager and a sync-gated bootstrap instead. A binding that owns undo via
|
|
308
|
+
// a SEPARATE `externalUndo` (Loro) has it wired here; one that owns undo
|
|
309
|
+
// inside its own `register` (Yjs) leaves the field unset. Either way the
|
|
310
|
+
// built-in history is off, so a binding that forgot both would show as "no
|
|
311
|
+
// undo" rather than fighting a local stack.
|
|
312
|
+
...(collabBinding
|
|
313
|
+
? {
|
|
314
|
+
history: false as const,
|
|
315
|
+
seedMode: 'deferred' as const,
|
|
316
|
+
...(collabBinding.externalUndo ? { externalUndo: collabBinding.externalUndo } : {}),
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
268
319
|
defaultValue: collabBinding || config.value ? undefined : (config.defaultValue ?? ''),
|
|
269
320
|
...(config.value && !collabBinding ? { value: config.value } : {}),
|
|
270
321
|
readonly: state.at('readonly'),
|
|
@@ -273,6 +324,10 @@ export function markdownEditor(
|
|
|
273
324
|
: {}),
|
|
274
325
|
register: (editor) => {
|
|
275
326
|
const disposers = [
|
|
327
|
+
// Keep the WYSIWYG surface and the serialized GFM dialect in lock-step:
|
|
328
|
+
// underline (Cmd+U) has no markdown representation, so it is swallowed
|
|
329
|
+
// rather than applied-then-silently-stripped on save.
|
|
330
|
+
blockUnderlineFormat(editor),
|
|
276
331
|
registerMarkdownShortcuts(editor, transformers),
|
|
277
332
|
// Global scheme allowlist for links: the single backstop covering the
|
|
278
333
|
// link dialog, pasted/imported markdown, and the typed `[x](url)`
|
|
@@ -297,7 +352,20 @@ export function markdownEditor(
|
|
|
297
352
|
}
|
|
298
353
|
config.onReady?.(editor)
|
|
299
354
|
},
|
|
300
|
-
onChange: (value) =>
|
|
355
|
+
onChange: (value) => {
|
|
356
|
+
// Deliver to the consumer DIRECTLY, independent of the TEA loop: the
|
|
357
|
+
// dispose-time debounce flush runs AFTER the component is disposed, and a
|
|
358
|
+
// `send` from a disposed loop is dropped — so routing consumer delivery
|
|
359
|
+
// through `send` would lose the last debounce window of typing on unmount.
|
|
360
|
+
// Deduped per mount so the same value isn't delivered twice.
|
|
361
|
+
if (value !== mount.lastChange) {
|
|
362
|
+
mount.lastChange = value
|
|
363
|
+
config.onChange?.(value)
|
|
364
|
+
}
|
|
365
|
+
// Mirror into TEA state (value/dirty) while the loop is alive; dropped
|
|
366
|
+
// silently after dispose, which is fine — the consumer was already told.
|
|
367
|
+
send({ type: 'markdownChanged', value })
|
|
368
|
+
},
|
|
301
369
|
onSelectionChange: (ctx) => {
|
|
302
370
|
const format = computeFormatState(ctx.editor, ctx)
|
|
303
371
|
const text = ctx.editor.getEditorState().read(() => $getRoot().getTextContent())
|
package/src/effects.ts
CHANGED
|
@@ -12,7 +12,6 @@ export interface EffectApi {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface EffectConfig {
|
|
15
|
-
onChange?: (markdown: string) => void
|
|
16
15
|
onFormatChange?: (format: FormatState) => void
|
|
17
16
|
/** Push markdown into the live editor (deserialize), without echoing onChange. */
|
|
18
17
|
applyValue: (editor: LexicalEditor, value: string) => void
|
|
@@ -41,7 +40,10 @@ export function makeOnEffect(
|
|
|
41
40
|
return
|
|
42
41
|
}
|
|
43
42
|
case 'emitChange': {
|
|
44
|
-
|
|
43
|
+
// Consumer `onChange` delivery moved to the foreign onChange wrapper (see
|
|
44
|
+
// editor.ts) so it survives dispose — the loop is torn down before the
|
|
45
|
+
// dispose-time debounce flush runs, and a `send`-routed effect would be
|
|
46
|
+
// dropped. This effect now only signals that state changed; no side effect.
|
|
45
47
|
return
|
|
46
48
|
}
|
|
47
49
|
case 'emitFormat': {
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
type CollabHooks,
|
|
8
8
|
type CollabFactory,
|
|
9
9
|
markdownEditor,
|
|
10
|
+
blockUnderlineFormat,
|
|
10
11
|
} from './editor.js'
|
|
11
12
|
|
|
12
13
|
export {
|
|
@@ -56,6 +57,17 @@ export {
|
|
|
56
57
|
$insertCallout,
|
|
57
58
|
} from './plugins/callout.js'
|
|
58
59
|
export { hrPlugin, $insertHorizontalRule } from './plugins/hr.js'
|
|
60
|
+
export {
|
|
61
|
+
type FrontmatterData,
|
|
62
|
+
type FrontmatterPluginOptions,
|
|
63
|
+
FRONTMATTER_BRIDGE_TYPE,
|
|
64
|
+
FRONTMATTER_TRANSFORMER,
|
|
65
|
+
frontmatterPlugin,
|
|
66
|
+
serializeFrontmatter,
|
|
67
|
+
splitFrontmatter,
|
|
68
|
+
$getFrontmatter,
|
|
69
|
+
$setFrontmatter,
|
|
70
|
+
} from './plugins/frontmatter.js'
|
|
59
71
|
export { slashPlugin } from './plugins/slash.js'
|
|
60
72
|
export { contextMenuPlugin } from './plugins/context-menu.js'
|
|
61
73
|
export { floatingToolbarPlugin } from './plugins/floating-toolbar.js'
|
|
@@ -65,21 +77,61 @@ export { type Mention, type MentionPluginOptions, mentionPlugin } from './plugin
|
|
|
65
77
|
export { type EmojiPluginOptions, DEFAULT_EMOJI, emojiPlugin } from './plugins/emoji.js'
|
|
66
78
|
export { type ImagePluginOptions, imagePlugin } from './plugins/image.js'
|
|
67
79
|
export { tablePlugin } from './plugins/table.js'
|
|
80
|
+
export {
|
|
81
|
+
type CodeLanguagePluginOptions,
|
|
82
|
+
type CodeLanguageState,
|
|
83
|
+
type CodeLanguageMsg,
|
|
84
|
+
type CodeLanguageEffect,
|
|
85
|
+
CODE_LANGUAGE_PLUGIN,
|
|
86
|
+
codeLanguagePlugin,
|
|
87
|
+
} from './plugins/code-language.js'
|
|
88
|
+
export {
|
|
89
|
+
type WikiLink,
|
|
90
|
+
type WikiLinkPluginOptions,
|
|
91
|
+
type SerializedWikiLinkNode,
|
|
92
|
+
WikiLinkNode,
|
|
93
|
+
$createWikiLinkNode,
|
|
94
|
+
$isWikiLinkNode,
|
|
95
|
+
parseWikiLinkInner,
|
|
96
|
+
formatWikiLink,
|
|
97
|
+
// The representability guards that make `formatWikiLink` the true inverse of
|
|
98
|
+
// `parseWikiLinkInner`; exported so a host resolving/creating targets can
|
|
99
|
+
// apply the same normalization before it hands one to `$createWikiLinkNode`.
|
|
100
|
+
sanitizeWikiLinkTarget,
|
|
101
|
+
sanitizeWikiLinkAlias,
|
|
102
|
+
wikilinkPlugin,
|
|
103
|
+
} from './plugins/wikilink.js'
|
|
104
|
+
export {
|
|
105
|
+
type BlockDragOptions,
|
|
106
|
+
type BlockRect,
|
|
107
|
+
type DropTarget,
|
|
108
|
+
type IndicatorRect,
|
|
109
|
+
type Place,
|
|
110
|
+
BLOCK_DRAG_Z,
|
|
111
|
+
blockAtPoint,
|
|
112
|
+
findDropTarget,
|
|
113
|
+
indicatorRect,
|
|
114
|
+
blockDragPlugin,
|
|
115
|
+
} from './plugins/block-drag.js'
|
|
68
116
|
|
|
69
117
|
export { $insertMarkdownAtSelection, registerMarkdownPaste } from './paste.js'
|
|
70
118
|
|
|
71
|
-
export { GFM_NODES, GFM_TRANSFORMERS } from './transformers/gfm.js'
|
|
72
|
-
|
|
119
|
+
export { GFM_NODES, GFM_TRANSFORMERS, HIGHLIGHT_TRANSFORMER } from './transformers/gfm.js'
|
|
120
|
+
// The CommonMark-correct fenced-code transformer. Already part of
|
|
121
|
+
// `GFM_TRANSFORMERS`; exported for consumers assembling a transformer set by hand.
|
|
122
|
+
export { CODE_INFO_TRANSFORMER, normalizeCodeInfo } from './transformers/code.js'
|
|
123
|
+
// `setTransformerPrecedence` breaks ties between SAME-rank transformers, so a
|
|
124
|
+
// collision (e.g. wikilink vs upstream LINK, which both match at the same index)
|
|
125
|
+
// is resolved structurally instead of by the order a consumer lists plugins in.
|
|
126
|
+
export {
|
|
127
|
+
buildTransformers,
|
|
128
|
+
orderTransformers,
|
|
129
|
+
setTransformerPrecedence,
|
|
130
|
+
} from './transformers/registry.js'
|
|
73
131
|
|
|
74
132
|
export { computeFormatState } from './format.js'
|
|
75
133
|
|
|
76
|
-
export {
|
|
77
|
-
STRIKETHROUGH_CLASS,
|
|
78
|
-
UNDERLINE_CLASS,
|
|
79
|
-
UNDERLINE_STRIKETHROUGH_CLASS,
|
|
80
|
-
defaultTheme,
|
|
81
|
-
mergeTheme,
|
|
82
|
-
} from './theme.js'
|
|
134
|
+
export { STRIKETHROUGH_CLASS, defaultTheme, mergeTheme } from './theme.js'
|
|
83
135
|
|
|
84
136
|
export {
|
|
85
137
|
type ToolbarItemParts,
|