@lexical/mdast 0.0.0-bootstrap.0 → 0.47.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.
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {CompiledMdast, MdastExportContext, MdastNode} from './types';
10
+ import type {BaseSelection, ElementNode, LexicalNode} from 'lexical';
11
+ import type {
12
+ BlockContent,
13
+ Break,
14
+ Code,
15
+ Heading,
16
+ Link,
17
+ List,
18
+ PhrasingContent,
19
+ Root,
20
+ RootContent,
21
+ ThematicBreak,
22
+ } from 'mdast';
23
+ import type {
24
+ Options as ToMarkdownExtension,
25
+ State,
26
+ } from 'mdast-util-to-markdown';
27
+
28
+ import {$sliceSelectedTextNodeContent} from '@lexical/selection';
29
+ import {
30
+ $getRoot,
31
+ $getSelection,
32
+ $getState,
33
+ $isElementNode,
34
+ $isLineBreakNode,
35
+ $isRangeSelection,
36
+ $isTextNode,
37
+ } from 'lexical';
38
+ import {defaultHandlers, toMarkdown} from 'mdast-util-to-markdown';
39
+ import {toString as mdastToString} from 'mdast-util-to-string';
40
+
41
+ import {
42
+ $exportLineBreak,
43
+ $isBlockLevelNode,
44
+ exportText,
45
+ phrasingFromFormattedText,
46
+ TEXT_FORMAT_MASK,
47
+ } from './handlers';
48
+ import {
49
+ emphasisMarkerState,
50
+ paragraphBreakState,
51
+ strongMarkerState,
52
+ } from './state';
53
+
54
+ /**
55
+ * Runs `fn` with `state.options` temporarily overridden, restoring the
56
+ * previous values afterwards so a per-node override never leaks into the rest
57
+ * of the serialization.
58
+ */
59
+ function withOptions<T>(
60
+ state: State,
61
+ overrides: State['options'],
62
+ fn: () => T,
63
+ ): T {
64
+ const {options} = state;
65
+ const saved: State['options'] = {};
66
+ for (const key of Object.keys(overrides) as (keyof State['options'])[]) {
67
+ saved[key] = options[key] as never;
68
+ options[key] = overrides[key] as never;
69
+ }
70
+ try {
71
+ return fn();
72
+ } finally {
73
+ for (const key of Object.keys(saved) as (keyof State['options'])[]) {
74
+ options[key] = saved[key] as never;
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * A to-markdown extension whose handlers reproduce the literal Markdown syntax
81
+ * captured on import (and stored on the Lexical nodes), by temporarily
82
+ * steering the default handlers with per-node options. Delegating to the
83
+ * defaults keeps all the indentation / nesting / disambiguation behavior
84
+ * intact while still honoring each node's original marker/fence. Nodes without
85
+ * captured syntax fall straight through to the defaults, so document-level
86
+ * options (and contributed `toMarkdownExtensions`) still apply to them.
87
+ */
88
+ const SYNTAX_TO_MARKDOWN: ToMarkdownExtension = {
89
+ handlers: {
90
+ break(node: Break, parent, state, info) {
91
+ // Delegate first: the default handler substitutes a space when a real
92
+ // EOL is unsafe in the current construct (headings, table cells). Only
93
+ // when it chose the hard-break form does the preserved marker apply:
94
+ // trailing-space markers are reproduced, and the empty marker means the
95
+ // break is SOFT (a source newline or editor line break) and serializes
96
+ // as a plain newline rather than being upgraded to a hard break.
97
+ const result = defaultHandlers.break(node, parent, state, info);
98
+ if (result !== '\\\n') {
99
+ return result;
100
+ }
101
+ const marker = node.data && node.data.mdastBreak;
102
+ if (!marker) {
103
+ return '\n';
104
+ }
105
+ return /^ {2,}$/.test(marker) ? `${marker}\n` : result;
106
+ },
107
+ code(node: Code, parent, state, info) {
108
+ const fence = node.data && node.data.mdastFence;
109
+ if (!fence) {
110
+ return defaultHandlers.code(node, parent, state, info);
111
+ }
112
+ return withOptions(
113
+ state,
114
+ {fence: fence[0] === '~' ? '~' : '`', fences: true},
115
+ () => defaultHandlers.code(node, parent, state, info),
116
+ );
117
+ },
118
+ heading(node: Heading, parent, state, info) {
119
+ // `data.mdastSetext` is only present (true) for imported setext
120
+ // headings; everything else defers to the document-level option.
121
+ if (!(node.data && node.data.mdastSetext === true)) {
122
+ return defaultHandlers.heading(node, parent, state, info);
123
+ }
124
+ return withOptions(state, {setext: true}, () =>
125
+ defaultHandlers.heading(node, parent, state, info),
126
+ );
127
+ },
128
+ link(node: Link, parent, state, info) {
129
+ // `data.mdastLinkStyle` preserves the syntax the link was written in.
130
+ // A title can't be expressed in autolink/literal form, so links with a
131
+ // title always fall through to the default (resource form).
132
+ const style = node.data && node.data.mdastLinkStyle;
133
+ if (style === 'literal' && node.title == null) {
134
+ // A bare GFM autolink literal: emit the visible text as-is. The
135
+ // gfm-autolink-literal to-markdown extension keeps surrounding
136
+ // escaping consistent so it re-parses as the same literal.
137
+ return mdastToString(node);
138
+ }
139
+ if (style === 'inline') {
140
+ // Force `[text](url)` even when the text equals the URL (the
141
+ // default handler would normalize that to an autolink).
142
+ return withOptions(state, {resourceLink: true}, () =>
143
+ defaultHandlers.link(node, parent, state, info),
144
+ );
145
+ }
146
+ return defaultHandlers.link(node, parent, state, info);
147
+ },
148
+ list(node: List, parent, state, info) {
149
+ if (node.ordered) {
150
+ const ordered = node.data && node.data.mdastBulletOrdered;
151
+ if (ordered == null) {
152
+ return defaultHandlers.list(node, parent, state, info);
153
+ }
154
+ return withOptions(state, {bulletOrdered: ordered}, () =>
155
+ defaultHandlers.list(node, parent, state, info),
156
+ );
157
+ }
158
+ const bullet = node.data && node.data.mdastBullet;
159
+ if (bullet == null) {
160
+ return defaultHandlers.list(node, parent, state, info);
161
+ }
162
+ return withOptions(
163
+ state,
164
+ {bullet, bulletOther: bullet === '-' ? '*' : '-'},
165
+ () => defaultHandlers.list(node, parent, state, info),
166
+ );
167
+ },
168
+ thematicBreak(node: ThematicBreak, parent, state) {
169
+ const marker = node.data && node.data.mdastRule;
170
+ if (marker !== '-' && marker !== '*' && marker !== '_') {
171
+ return defaultHandlers.thematicBreak(node, parent, state);
172
+ }
173
+ return withOptions(state, {rule: marker}, () =>
174
+ defaultHandlers.thematicBreak(node, parent, state),
175
+ );
176
+ },
177
+ },
178
+ };
179
+
180
+ /**
181
+ * Accumulates adjacent plain text nodes that share a format so they serialize
182
+ * to a single delimiter pair (e.g. `**ab**` rather than `**a****b**`). Shared
183
+ * by the inline and block export walks.
184
+ */
185
+ class TextRunAccumulator {
186
+ private format = -1;
187
+ private value = '';
188
+
189
+ /**
190
+ * Returns `true` when `child` was absorbed. A format change flushes the
191
+ * previous run into `out` first. The caller decides *which* text nodes
192
+ * are eligible (plain text with no export rule of its own); this only
193
+ * guards the node kind.
194
+ */
195
+ push(child: LexicalNode, out: PhrasingContent[]): boolean {
196
+ if (!$isTextNode(child)) {
197
+ return false;
198
+ }
199
+ const format = child.getFormat() & TEXT_FORMAT_MASK;
200
+ if (format === this.format) {
201
+ this.value += child.getTextContent();
202
+ } else {
203
+ this.flushInto(out);
204
+ this.format = format;
205
+ this.value = child.getTextContent();
206
+ }
207
+ return true;
208
+ }
209
+
210
+ flushInto(out: PhrasingContent[]): void {
211
+ if (this.format >= 0) {
212
+ out.push(phrasingFromFormattedText(this.value, this.format));
213
+ }
214
+ this.format = -1;
215
+ this.value = '';
216
+ }
217
+ }
218
+
219
+ function createNodeExporter(
220
+ compiled: CompiledMdast,
221
+ selection: BaseSelection | null = null,
222
+ ) {
223
+ const {exportHandlers} = compiled;
224
+ // Incremented whenever a selected leaf contributes output. Parents compare
225
+ // it before/after recursing to keep an element that is not itself selected
226
+ // but hosts selected content (the markdown analogue of extractWithChild).
227
+ let selectionHits = 0;
228
+
229
+ /**
230
+ * Whether the run accumulator may absorb `child`: a text node whose only
231
+ * export behavior would be the core text fallback. A node whose type has
232
+ * its own registered rule (or the core `'text'` rule, which the
233
+ * accumulator supersedes to merge adjacent runs) must not be swallowed —
234
+ * this keeps replaced/custom text nodes dispatching to their handlers.
235
+ */
236
+ function mayAccumulate(child: LexicalNode): boolean {
237
+ const handler = exportHandlers.get(child.getType());
238
+ return handler === undefined || handler === exportText;
239
+ }
240
+
241
+ /**
242
+ * Selection filter for one child. With no selection every child passes
243
+ * through unchanged. Leaves (text, line break, decorator) pass only when
244
+ * selected, a partially selected text node as a detached clone sliced to
245
+ * the selected range; `null` means skip. Elements always pass — they are
246
+ * judged after recursion by {@link $dispatchElement}.
247
+ */
248
+ function $filterChild(child: LexicalNode): LexicalNode | null {
249
+ if (selection === null || $isElementNode(child)) {
250
+ return child;
251
+ }
252
+ if (!child.isSelected(selection)) {
253
+ return null;
254
+ }
255
+ selectionHits++;
256
+ return $isTextNode(child)
257
+ ? $sliceSelectedTextNodeContent(selection, child, 'clone')
258
+ : child;
259
+ }
260
+
261
+ /**
262
+ * Whether `node` or any descendant is selected. See
263
+ * {@link MdastExportContext.isIncluded}.
264
+ */
265
+ function $isIncluded(node: LexicalNode): boolean {
266
+ if (selection === null || node.isSelected(selection)) {
267
+ return true;
268
+ }
269
+ if ($isElementNode(node)) {
270
+ for (const child of node.getChildren()) {
271
+ if ($isIncluded(child)) {
272
+ return true;
273
+ }
274
+ }
275
+ }
276
+ return false;
277
+ }
278
+
279
+ /**
280
+ * Dispatches an element child, appending its output to `out` unless a
281
+ * selection is active and neither the element nor any descendant is
282
+ * selected.
283
+ */
284
+ function $dispatchElement(child: LexicalNode, out: MdastNode[]): void {
285
+ const selfSelected = selection !== null && child.isSelected(selection);
286
+ if (selfSelected) {
287
+ selectionHits++;
288
+ }
289
+ const before = selectionHits;
290
+ const result = $dispatch(child);
291
+ if (selection === null || selfSelected || selectionHits > before) {
292
+ out.push(...result);
293
+ }
294
+ }
295
+
296
+ const context: MdastExportContext = {
297
+ exportBlocks: node => $exportBlocks(node),
298
+ exportChildren: node => {
299
+ const out: MdastNode[] = [];
300
+ for (const child of node.getChildren()) {
301
+ const target = $filterChild(child);
302
+ if (target === null) {
303
+ continue;
304
+ }
305
+ if ($isElementNode(target)) {
306
+ $dispatchElement(target, out);
307
+ } else {
308
+ out.push(...$dispatch(target));
309
+ }
310
+ }
311
+ return out;
312
+ },
313
+ exportInline: node => $exportInline(node),
314
+ isIncluded: $isIncluded,
315
+ };
316
+
317
+ function $dispatch(node: LexicalNode): MdastNode[] {
318
+ const handler = exportHandlers.get(node.getType());
319
+ if (handler) {
320
+ const result = handler(node, context);
321
+ if (result != null) {
322
+ return Array.isArray(result) ? result : [result];
323
+ }
324
+ }
325
+ // Fallbacks keep unknown nodes from disappearing entirely; text and line
326
+ // breaks reuse the core handlers so their behavior can't drift.
327
+ const asText = exportText(node);
328
+ if (asText !== null) {
329
+ return [asText];
330
+ }
331
+ const asBreak = $exportLineBreak(node);
332
+ if (asBreak !== null) {
333
+ return [asBreak];
334
+ }
335
+ if ($isElementNode(node)) {
336
+ return context.exportChildren(node);
337
+ }
338
+ const text = node.getTextContent();
339
+ return text ? [{type: 'text', value: text}] : [];
340
+ }
341
+
342
+ /**
343
+ * Converts the inline children of `node` into phrasing content.
344
+ */
345
+ function $exportInline(node: ElementNode): PhrasingContent[] {
346
+ const result: PhrasingContent[] = [];
347
+ const runs = new TextRunAccumulator();
348
+ for (const child of node.getChildren()) {
349
+ const target = $filterChild(child);
350
+ if (target === null) {
351
+ continue;
352
+ }
353
+ if (!(mayAccumulate(target) && runs.push(target, result))) {
354
+ runs.flushInto(result);
355
+ if ($isElementNode(target)) {
356
+ // The registry erases types; phrasing output is the dispatch
357
+ // contract for inline children.
358
+ $dispatchElement(target, result as MdastNode[]);
359
+ } else {
360
+ result.push(...($dispatch(target) as PhrasingContent[]));
361
+ }
362
+ }
363
+ }
364
+ runs.flushInto(result);
365
+ return result;
366
+ }
367
+
368
+ /**
369
+ * Converts a container whose Lexical children are inline (block quote, list
370
+ * item) into mdast block content. A LineBreakNode marked as a paragraph
371
+ * boundary (set by the import handlers when joining sibling paragraphs)
372
+ * splits the content; any other LineBreakNode stays an inline `break`
373
+ * (hard or soft according to its marker). Nested block children pass
374
+ * through directly.
375
+ */
376
+ function $exportBlocks(node: ElementNode): BlockContent[] {
377
+ const blocks: BlockContent[] = [];
378
+ let inline: PhrasingContent[] = [];
379
+ const runs = new TextRunAccumulator();
380
+ const flushParagraph = () => {
381
+ runs.flushInto(inline);
382
+ if (inline.length > 0) {
383
+ blocks.push({children: inline, type: 'paragraph'});
384
+ inline = [];
385
+ }
386
+ };
387
+ for (const child of node.getChildren()) {
388
+ const target = $filterChild(child);
389
+ if (target === null) {
390
+ continue;
391
+ }
392
+ if ($isLineBreakNode(target)) {
393
+ const asBreak = $exportLineBreak(target);
394
+ if ($getState(target, paragraphBreakState) || asBreak === null) {
395
+ flushParagraph();
396
+ } else {
397
+ runs.flushInto(inline);
398
+ inline.push(asBreak);
399
+ }
400
+ } else if (mayAccumulate(target) && runs.push(target, inline)) {
401
+ continue;
402
+ } else if ($isBlockLevelNode(target)) {
403
+ flushParagraph();
404
+ if ($isElementNode(target)) {
405
+ // The registry erases types; block-level output is the dispatch
406
+ // contract for block children.
407
+ $dispatchElement(target, blocks as MdastNode[]);
408
+ } else {
409
+ blocks.push(...($dispatch(target) as BlockContent[]));
410
+ }
411
+ } else if ($isElementNode(target)) {
412
+ runs.flushInto(inline);
413
+ $dispatchElement(target, inline as MdastNode[]);
414
+ } else {
415
+ runs.flushInto(inline);
416
+ inline.push(...($dispatch(target) as PhrasingContent[]));
417
+ }
418
+ }
419
+ flushParagraph();
420
+ if (blocks.length === 0) {
421
+ blocks.push({children: [], type: 'paragraph'});
422
+ }
423
+ return blocks;
424
+ }
425
+
426
+ return {exportChildren: context.exportChildren};
427
+ }
428
+
429
+ /**
430
+ * Picks the document-level emphasis and strong delimiters from the first
431
+ * italic / bold text node that recorded a known delimiter on import, scanning
432
+ * the tree rooted at `node`. Mixing delimiters within one document is not
433
+ * supported by to-markdown's escaping, so a single choice is made per
434
+ * document; nodes with no recorded delimiter (created in the editor) are
435
+ * skipped so they cannot mask the document's authored style.
436
+ */
437
+ function $dominantInlineMarkers(node: ElementNode): {
438
+ emphasis: '*' | '_' | undefined;
439
+ strong: '*' | '_' | undefined;
440
+ } {
441
+ let emphasis: '*' | '_' | undefined;
442
+ let strong: '*' | '_' | undefined;
443
+ const visit = (element: ElementNode): void => {
444
+ for (const child of element.getChildren()) {
445
+ if ($isTextNode(child)) {
446
+ if (emphasis === undefined && child.hasFormat('italic')) {
447
+ const marker = $getState(child, emphasisMarkerState);
448
+ if (marker === '_') {
449
+ emphasis = '_';
450
+ }
451
+ }
452
+ if (strong === undefined && child.hasFormat('bold')) {
453
+ const marker = $getState(child, strongMarkerState);
454
+ if (marker === '_') {
455
+ strong = '_';
456
+ }
457
+ }
458
+ } else if ($isElementNode(child)) {
459
+ visit(child);
460
+ }
461
+ if (emphasis !== undefined && strong !== undefined) {
462
+ return;
463
+ }
464
+ }
465
+ };
466
+ visit(node);
467
+ return {emphasis, strong};
468
+ }
469
+
470
+ /**
471
+ * Creates a reusable exporter that converts the Lexical tree rooted at the
472
+ * supplied element (or the editor root) — or just the selected content —
473
+ * into a Markdown string.
474
+ */
475
+ export function createMdastExport(compiled: CompiledMdast): {
476
+ $exportToMdast: (node?: ElementNode) => Root;
477
+ $exportToMarkdown: (node?: ElementNode) => string;
478
+ $exportSelectionToMarkdown: (selection?: BaseSelection | null) => string;
479
+ } {
480
+ // The unfiltered exporter has no per-call state and is shared by every
481
+ // whole-document export; selection exports build a fresh one per call.
482
+ const documentExporter = createNodeExporter(compiled);
483
+
484
+ const $toMdast = (
485
+ root: ElementNode,
486
+ exporter: ReturnType<typeof createNodeExporter>,
487
+ ): Root => ({
488
+ // The registry erases types; root-level output is the dispatch contract
489
+ // for top-level children.
490
+ children: exporter.exportChildren(root) as RootContent[],
491
+ type: 'root',
492
+ });
493
+
494
+ const $serialize = (tree: Root, root: ElementNode): string => {
495
+ // Emphasis/strong delimiters are document-level; the delimiter recorded on
496
+ // import wins, otherwise contributed toMarkdownExtensions (and the '-'
497
+ // bullet baseline) decide. Defaults ride as the FIRST extension so that
498
+ // contributed extensions can override them; SYNTAX_TO_MARKDOWN runs last
499
+ // so its handlers reproduce the per-node syntax captured on import.
500
+ const {emphasis, strong} = $dominantInlineMarkers(root);
501
+ const defaults: ToMarkdownExtension = {bullet: '-'};
502
+ if (emphasis) {
503
+ defaults.emphasis = emphasis;
504
+ }
505
+ if (strong) {
506
+ defaults.strong = strong;
507
+ }
508
+ const out = toMarkdown(tree, {
509
+ extensions: [
510
+ defaults,
511
+ ...compiled.toMarkdownExtensions,
512
+ SYNTAX_TO_MARKDOWN,
513
+ ],
514
+ });
515
+ // toMarkdown always appends a trailing newline; drop it so callers get the
516
+ // same shape as `@lexical/markdown`'s `$convertToMarkdownString`.
517
+ return out.replace(/\n$/, '');
518
+ };
519
+
520
+ const $exportToMdast = (node?: ElementNode): Root =>
521
+ $toMdast(node || $getRoot(), documentExporter);
522
+
523
+ const $exportToMarkdown = (node?: ElementNode): string => {
524
+ const root = node || $getRoot();
525
+ return $serialize($toMdast(root, documentExporter), root);
526
+ };
527
+
528
+ const $exportSelectionToMarkdown = (
529
+ selection: BaseSelection | null = $getSelection(),
530
+ ): string => {
531
+ if (
532
+ selection === null ||
533
+ ($isRangeSelection(selection) && selection.isCollapsed())
534
+ ) {
535
+ return '';
536
+ }
537
+ const root = $getRoot();
538
+ return $serialize(
539
+ $toMdast(root, createNodeExporter(compiled, selection)),
540
+ root,
541
+ );
542
+ };
543
+
544
+ return {$exportSelectionToMarkdown, $exportToMarkdown, $exportToMdast};
545
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {BaseSelection, ElementNode} from 'lexical';
10
+ import type {Root} from 'mdast';
11
+
12
+ import {$getExtensionOutput} from '@lexical/extension';
13
+ import {defineExtension} from 'lexical';
14
+
15
+ import {createMdastExport} from './MdastExport';
16
+ import {MdastImportExtension} from './MdastImportExtension';
17
+
18
+ /**
19
+ * The runtime API exposed by {@link MdastExportExtension}. Obtain it inside a
20
+ * read/update with `$getExtensionOutput(MdastExportExtension)`, or use the
21
+ * {@link $convertToMarkdownString} shorthand.
22
+ * @experimental
23
+ */
24
+ export interface MdastExportExtensionOutput {
25
+ /**
26
+ * Serializes the editor root (or `node`) to a Markdown string. Must be
27
+ * called inside an `editor.read()` or `editor.update()`.
28
+ */
29
+ $convertToMarkdownString(node?: ElementNode): string;
30
+ /**
31
+ * Exports the editor root (or `node`) to an mdast `Root` tree without
32
+ * serializing it, for interop with the unified/remark ecosystem (remark
33
+ * plugins, `remark-rehype`, tree diffing, ...). Must be called inside an
34
+ * `editor.read()` or `editor.update()`. Syntax preserved from import
35
+ * rides along as `data` fields on the nodes, mdast's sanctioned
36
+ * extension point.
37
+ */
38
+ $convertToMdast(node?: ElementNode): Root;
39
+ /**
40
+ * Serializes only the selected content (defaulting to the current
41
+ * selection) to a Markdown string: leaves outside the selection are
42
+ * skipped, partially selected text nodes are sliced to the selected
43
+ * range, and elements are kept when they or any descendant are selected.
44
+ * Returns `''` for a null or collapsed selection. Must be called inside
45
+ * an `editor.read()` or `editor.update()`.
46
+ */
47
+ $convertSelectionToMarkdownString(selection?: BaseSelection | null): string;
48
+ }
49
+
50
+ /**
51
+ * Markdown serialization for `@lexical/mdast`. Import
52
+ * (`MdastImportExtension` and the feature extensions that contribute to it) and
53
+ * export are separate extensions so that editors which only *parse* Markdown
54
+ * — never serialize back — don't bundle `mdast-util-to-markdown`.
55
+ *
56
+ * The export rules themselves are contributed by the same feature extensions
57
+ * that contribute import rules; this extension compiles the shared registry
58
+ * into a serializer:
59
+ * ```ts
60
+ * dependencies: [MdastCommonMarkExtension, MdastExportExtension]
61
+ * ```
62
+ * @experimental
63
+ */
64
+ export const MdastExportExtension = /* @__PURE__ */ defineExtension<
65
+ Record<never, never>,
66
+ '@lexical/mdast/Export',
67
+ MdastExportExtensionOutput,
68
+ void
69
+ >({
70
+ build(editor, config, state): MdastExportExtensionOutput {
71
+ const {registry} = state.getDependency(MdastImportExtension).output;
72
+ const {$exportSelectionToMarkdown, $exportToMdast, $exportToMarkdown} =
73
+ createMdastExport(registry);
74
+ return {
75
+ $convertSelectionToMarkdownString: $exportSelectionToMarkdown,
76
+ $convertToMarkdownString: $exportToMarkdown,
77
+ $convertToMdast: $exportToMdast,
78
+ };
79
+ },
80
+ dependencies: [MdastImportExtension],
81
+ name: '@lexical/mdast/Export',
82
+ });
83
+
84
+ /**
85
+ * Shorthand for
86
+ * `$getExtensionOutput(MdastExportExtension).$convertToMarkdownString`.
87
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
88
+ * the editor was not built with {@link MdastExportExtension}.
89
+ * @experimental
90
+ */
91
+ export function $convertToMarkdownString(node?: ElementNode): string {
92
+ return $getExtensionOutput(MdastExportExtension).$convertToMarkdownString(
93
+ node,
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Shorthand for `$getExtensionOutput(MdastExportExtension).$convertToMdast`.
99
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
100
+ * the editor was not built with {@link MdastExportExtension}.
101
+ * @experimental
102
+ */
103
+ export function $convertToMdast(node?: ElementNode): Root {
104
+ return $getExtensionOutput(MdastExportExtension).$convertToMdast(node);
105
+ }
106
+
107
+ /**
108
+ * Shorthand for
109
+ * `$getExtensionOutput(MdastExportExtension).$convertSelectionToMarkdownString`.
110
+ * Serializes only the selected content (defaulting to the current selection)
111
+ * to a Markdown string; returns `''` for a null or collapsed selection.
112
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
113
+ * the editor was not built with {@link MdastExportExtension}.
114
+ * @experimental
115
+ */
116
+ export function $convertSelectionToMarkdownString(
117
+ selection?: BaseSelection | null,
118
+ ): string {
119
+ return $getExtensionOutput(
120
+ MdastExportExtension,
121
+ ).$convertSelectionToMarkdownString(selection);
122
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import {defineExtension} from 'lexical';
10
+
11
+ import {MdastExportExtension} from './MdastExportExtension';
12
+ import {MdastImportExtension} from './MdastImportExtension';
13
+
14
+ /**
15
+ * Convenience bundle of {@link MdastImportExtension} and
16
+ * {@link MdastExportExtension}: Markdown parsing *and* serialization.
17
+ *
18
+ * Depend on this when you want both directions without thinking about it:
19
+ * ```ts
20
+ * dependencies: [MdastCommonMarkExtension, MdastExtension]
21
+ * ```
22
+ * Editors that never serialize back to Markdown can skip it (feature
23
+ * extensions already pull in {@link MdastImportExtension}) and avoid
24
+ * bundling the serializer (`mdast-util-to-markdown`).
25
+ * @experimental
26
+ */
27
+ export const MdastExtension = /* @__PURE__ */ defineExtension({
28
+ dependencies: [MdastImportExtension, MdastExportExtension],
29
+ name: '@lexical/mdast/Mdast',
30
+ });