@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,2620 @@
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
+ 'use strict';
10
+
11
+ var extension = require('@lexical/extension');
12
+ var lexical = require('lexical');
13
+ var selection = require('@lexical/selection');
14
+ var mdastUtilToMarkdown = require('mdast-util-to-markdown');
15
+ var mdastUtilToString = require('mdast-util-to-string');
16
+ var codeCore = require('@lexical/code-core');
17
+ var link = require('@lexical/link');
18
+ var list = require('@lexical/list');
19
+ var richText = require('@lexical/rich-text');
20
+ var mdastUtilGfmAutolinkLiteral = require('mdast-util-gfm-autolink-literal');
21
+ var mdastUtilGfmStrikethrough = require('mdast-util-gfm-strikethrough');
22
+ var mdastUtilGfmTaskListItem = require('mdast-util-gfm-task-list-item');
23
+ var micromarkExtensionGfmAutolinkLiteral = require('micromark-extension-gfm-autolink-literal');
24
+ var micromarkExtensionGfmStrikethrough = require('micromark-extension-gfm-strikethrough');
25
+ var micromarkExtensionGfmTaskListItem = require('micromark-extension-gfm-task-list-item');
26
+ var mdastUtilFromMarkdown = require('mdast-util-from-markdown');
27
+ var table = require('@lexical/table');
28
+ var mdastUtilGfmTable = require('mdast-util-gfm-table');
29
+ var micromarkExtensionGfmTable = require('micromark-extension-gfm-table');
30
+
31
+ /**
32
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
33
+ *
34
+ * This source code is licensed under the MIT license found in the
35
+ * LICENSE file in the root directory of this source tree.
36
+ *
37
+ */
38
+
39
+
40
+ /**
41
+ * Per-node state used to round-trip the *exact* Markdown syntax a construct was
42
+ * parsed from, so re-serializing produces minimally different output. This is
43
+ * the same technique `@lexical/markdown` uses; the state lives on the Lexical
44
+ * nodes (and therefore survives serialization), and the exporter reads it to
45
+ * reproduce the original marker/fence/break.
46
+ *
47
+ * All of these default to the empty sentinel (`''` / `false`) meaning
48
+ * "unknown" — i.e. the node was not created by a Markdown import. The exporter
49
+ * only pins a node's syntax when the marker is known, so nodes created in the
50
+ * editor defer to the document-level serialization options.
51
+ */
52
+
53
+ /** The bullet character (`-`, `*`, `+`) an unordered/check `ListNode` used. */
54
+ const listMarkerState = /* @__PURE__ */lexical.createState('mdastListMarker', {
55
+ parse: v => v === '-' || v === '*' || v === '+' ? v : '',
56
+ resetOnCopyNode: true
57
+ });
58
+
59
+ /** The delimiter (`.` or `)`) an ordered `ListNode` used. */
60
+ const orderedMarkerState = /* @__PURE__ */lexical.createState('mdastOrderedMarker', {
61
+ parse: v => v === '.' || v === ')' ? v : '',
62
+ resetOnCopyNode: true
63
+ });
64
+
65
+ /** The marker (`_`) an italic run used when it was not the default `*`. */
66
+ const emphasisMarkerState = /* @__PURE__ */lexical.createState('mdastEmphasisMarker', {
67
+ parse: v => v === '_' ? '_' : '',
68
+ resetOnCopyNode: true
69
+ });
70
+
71
+ /** The marker (`_`) a bold run used when it was not the default `*`. */
72
+ const strongMarkerState = /* @__PURE__ */lexical.createState('mdastStrongMarker', {
73
+ parse: v => v === '_' ? '_' : '',
74
+ resetOnCopyNode: true
75
+ });
76
+
77
+ /** Whether a (level 1/2) `HeadingNode` was written in setext style. */
78
+ const setextState = /* @__PURE__ */lexical.createState('mdastSetext', {
79
+ parse: v => v === true,
80
+ resetOnCopyNode: true
81
+ });
82
+
83
+ /** The fence a `CodeNode` used (e.g. ```` ``` ````, ````` ```` `````, `~~~`). */
84
+ const codeFenceState = /* @__PURE__ */lexical.createState('mdastCodeFence', {
85
+ parse: v => typeof v === 'string' && /^(`{3,}|~{3,})$/.test(v) ? v : '',
86
+ resetOnCopyNode: true
87
+ });
88
+
89
+ /**
90
+ * The info-string tail after a `CodeNode`'s language (e.g. `title=x` in
91
+ * ```` ```js title=x ````). `CodeNode` itself only models the language;
92
+ * this keeps the rest of the info string so it survives the round-trip.
93
+ */
94
+ const codeMetaState = /* @__PURE__ */lexical.createState('mdastCodeMeta', {
95
+ parse: v => typeof v === 'string' ? v : '',
96
+ resetOnCopyNode: true
97
+ });
98
+
99
+ /**
100
+ * The hard-line-break marker a `LineBreakNode` used (`\` or trailing spaces).
101
+ * The empty sentinel means the break is *soft* (a source newline or an
102
+ * editor-created line break) and serializes as a plain newline.
103
+ */
104
+ const hardLineBreakState = /* @__PURE__ */lexical.createState('mdastHardLineBreak', {
105
+ parse: v => typeof v === 'string' && /^(\\| {2,})$/.test(v) ? v : '',
106
+ resetOnCopyNode: true
107
+ });
108
+
109
+ /**
110
+ * Marks a `LineBreakNode` that stands for a *paragraph boundary* inside a
111
+ * container whose Lexical children are inline (blockquote, list item). Set by
112
+ * the import handlers when they join sibling mdast paragraphs; the exporter
113
+ * splits on it to reconstruct the paragraphs.
114
+ */
115
+ const paragraphBreakState = /* @__PURE__ */lexical.createState('mdastParagraphBreak', {
116
+ parse: v => v === true,
117
+ resetOnCopyNode: true
118
+ });
119
+
120
+ /** The marker (`-`, `*`, `_`) a thematic break / `HorizontalRuleNode` used. */
121
+ const hrMarkerState = /* @__PURE__ */lexical.createState('mdastHrMarker', {
122
+ parse: v => v === '-' || v === '*' || v === '_' ? v : '',
123
+ resetOnCopyNode: true
124
+ });
125
+
126
+ /**
127
+ * The syntax a `LinkNode` was written in: `'inline'` (`[text](url)`),
128
+ * `'autolink'` (`<url>`), or `'literal'` (a bare GFM autolink literal,
129
+ * `https://…` in prose).
130
+ */
131
+ const linkStyleState = /* @__PURE__ */lexical.createState('mdastLinkStyle', {
132
+ parse: v => v === 'inline' || v === 'autolink' || v === 'literal' ? v : '',
133
+ resetOnCopyNode: true
134
+ });
135
+
136
+ /**
137
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
138
+ *
139
+ * This source code is licensed under the MIT license found in the
140
+ * LICENSE file in the root directory of this source tree.
141
+ *
142
+ */
143
+
144
+
145
+ /**
146
+ * Appends `nodes` to `element` via {@link ElementNode.splice} (the primitive
147
+ * `append` delegates to, taking an array directly), returning the element.
148
+ */
149
+ function $append(element, nodes) {
150
+ return element.splice(element.getChildrenSize(), 0, nodes);
151
+ }
152
+
153
+ /**
154
+ * Prepends `nodes` to `element` via {@link ElementNode.splice} (the primitive
155
+ * `append` delegates to, taking an array directly), returning the element.
156
+ */
157
+ function $prepend(element, nodes) {
158
+ return element.splice(0, 0, nodes);
159
+ }
160
+
161
+ /**
162
+ * Whether `node` is a block-level node: a non-inline element *or* a non-inline
163
+ * decorator (e.g. a horizontal rule).
164
+ */
165
+ function $isBlockLevelNode(node) {
166
+ return (lexical.$isElementNode(node) || lexical.$isDecoratorNode(node)) && !node.isInline();
167
+ }
168
+
169
+ /** A `LineBreakNode` marking a paragraph boundary inside a container. */
170
+ function $createParagraphBreakNode() {
171
+ return lexical.$setState(lexical.$createLineBreakNode(), paragraphBreakState, true);
172
+ }
173
+
174
+ /** Reads the first source character of `node` (an inline delimiter, if any). */
175
+ function inlineMarker(ctx, node) {
176
+ if (!ctx.source || !node.position || node.position.start.offset == null) {
177
+ return undefined;
178
+ }
179
+ return ctx.source[node.position.start.offset];
180
+ }
181
+ const FORMAT_BOLD = lexical.TEXT_TYPE_TO_FORMAT.bold;
182
+ const FORMAT_ITALIC = lexical.TEXT_TYPE_TO_FORMAT.italic;
183
+ const FORMAT_STRIKETHROUGH = lexical.TEXT_TYPE_TO_FORMAT.strikethrough;
184
+ const FORMAT_CODE = lexical.TEXT_TYPE_TO_FORMAT.code;
185
+ const TEXT_FORMAT_MASK = FORMAT_BOLD | FORMAT_ITALIC | FORMAT_STRIKETHROUGH | FORMAT_CODE;
186
+
187
+ /* -------------------------------------------------------------------------- *
188
+ * Import handlers: mdast node -> Lexical node(s) *
189
+ * -------------------------------------------------------------------------- */
190
+
191
+ const $importParagraph = (node, ctx) => $append(lexical.$createParagraphNode(), ctx.importChildren(node));
192
+ const $importHeading = (node, ctx) => {
193
+ const heading = richText.$createHeadingNode(`h${node.depth}`);
194
+ // A level 1/2 heading that does not start with a valid ATX marker (`#`..
195
+ // `######` followed by whitespace or end of line) was written in setext
196
+ // style. Checking for the trailing boundary matters: `#foo\n===` is a
197
+ // setext heading whose *content* begins with `#`.
198
+ if (ctx.source && node.position && (node.depth === 1 || node.depth === 2)) {
199
+ const offset = node.position.start.offset;
200
+ if (offset != null && !/^ {0,3}#{1,6}([ \t\r\n]|$)/.test(ctx.source.slice(offset, offset + 10))) {
201
+ lexical.$setState(heading, setextState, true);
202
+ }
203
+ }
204
+ return $append(heading, ctx.importChildren(node));
205
+ };
206
+ const $importBlockquote = (node, ctx) => {
207
+ const quote = richText.$createQuoteNode();
208
+ const children = [];
209
+ for (const child of node.children) {
210
+ if (child.type === 'paragraph') {
211
+ if (children.length > 0) {
212
+ children.push($createParagraphBreakNode());
213
+ }
214
+ children.push(...ctx.importChildren(child));
215
+ } else {
216
+ // Nested blocks (lists, code, nested quotes) are imported as-is so they
217
+ // are not silently flattened to text.
218
+ children.push(...ctx.importNode(child));
219
+ }
220
+ }
221
+ return $append(quote, children);
222
+ };
223
+
224
+ /**
225
+ * Imports each mdast flow child of `parent` as block-level Lexical nodes,
226
+ * wrapping any stray inline output (e.g. from the `html` fallback) in a
227
+ * paragraph — the same normalization the top-level importer applies.
228
+ */
229
+ function $importBlockChildren(parent, ctx) {
230
+ const blocks = [];
231
+ let pendingParagraph = null;
232
+ const flushPending = () => {
233
+ if (pendingParagraph) {
234
+ blocks.push(pendingParagraph);
235
+ pendingParagraph = null;
236
+ }
237
+ };
238
+ for (const child of parent.children) {
239
+ for (const node of ctx.importNode(child)) {
240
+ if ($isBlockLevelNode(node)) {
241
+ flushPending();
242
+ blocks.push(node);
243
+ } else {
244
+ if (!pendingParagraph) {
245
+ pendingParagraph = lexical.$createParagraphNode();
246
+ }
247
+ $append(pendingParagraph, [node]);
248
+ }
249
+ }
250
+ }
251
+ flushPending();
252
+ return blocks;
253
+ }
254
+
255
+ /**
256
+ * Opt-in replacement for {@link $importBlockquote} that imports the quote as a
257
+ * shadow root {@link QuoteNode} holding block-level children, so structured
258
+ * blockquotes (multiple paragraphs, nested lists, code) round-trip without
259
+ * being flattened to inline content. See `MdastShadowRootQuoteExtension`.
260
+ */
261
+ const $importShadowRootBlockquote = (node, ctx) => $append(richText.$createQuoteNode({
262
+ shadowRoot: true
263
+ }), $importBlockChildren(node, ctx));
264
+
265
+ /** Maps an mdast `list` node to the Lexical {@link ListType} it represents. */
266
+ function $listTypeFromMdast(node) {
267
+ if (node.ordered) {
268
+ return 'number';
269
+ }
270
+ for (const child of node.children) {
271
+ if (child.type === 'listItem' && child.checked != null) {
272
+ return 'check';
273
+ }
274
+ }
275
+ return 'bullet';
276
+ }
277
+ const $importList = (node, ctx) => {
278
+ const listType = $listTypeFromMdast(node);
279
+ const start = node.ordered && node.start != null ? node.start : 1;
280
+ const list$1 = list.$createListNode(listType, start);
281
+ // Preserve the literal marker the list used so export can reproduce it.
282
+ // A bounded window is enough: an ordered marker is at most 9 digits plus
283
+ // the delimiter (CommonMark), a bullet is a single character.
284
+ const firstItem = node.children[0];
285
+ const itemOffset = ctx.source && firstItem && firstItem.position ? firstItem.position.start.offset : undefined;
286
+ if (itemOffset != null) {
287
+ const markerWindow = ctx.source.slice(itemOffset, itemOffset + 16);
288
+ if (listType === 'number') {
289
+ const match = markerWindow.match(/^\s*\d+([.)])/);
290
+ const delimiter = match && match[1];
291
+ if (delimiter === '.' || delimiter === ')') {
292
+ lexical.$setState(list$1, orderedMarkerState, delimiter);
293
+ }
294
+ } else {
295
+ const match = markerWindow.match(/^\s*([-*+])/);
296
+ const bullet = match && match[1];
297
+ if (bullet === '-' || bullet === '*' || bullet === '+') {
298
+ lexical.$setState(list$1, listMarkerState, bullet);
299
+ }
300
+ }
301
+ }
302
+ return $append(list$1, node.children.flatMap(child => ctx.importNode(child)));
303
+ };
304
+ const $importListItem = (node, ctx) => {
305
+ const item = list.$createListItemNode(typeof node.checked === 'boolean' ? node.checked : undefined);
306
+ const extraItems = [];
307
+ for (const child of node.children) {
308
+ if (child.type === 'list') {
309
+ // A nested list is represented in Lexical as a ListItemNode whose only
310
+ // child is the nested ListNode, appended as a sibling of this item.
311
+ extraItems.push($append(list.$createListItemNode(), ctx.importNode(child)));
312
+ } else if (child.type === 'paragraph') {
313
+ if (item.getChildrenSize() > 0) {
314
+ $append(item, [$createParagraphBreakNode()]);
315
+ }
316
+ $append(item, ctx.importChildren(child));
317
+ } else {
318
+ $append(item, ctx.importNode(child));
319
+ }
320
+ }
321
+ return [item, ...extraItems];
322
+ };
323
+ const $importCode = (node, ctx) => {
324
+ const code = codeCore.$createCodeNode(node.lang || undefined);
325
+ // Preserve the literal fence (e.g. ``` vs ~~~ vs ````) for round-tripping.
326
+ // The fence is on the construct's first line; bound the scan to it.
327
+ if (ctx.source && node.position && node.position.start.offset != null) {
328
+ const offset = node.position.start.offset;
329
+ const lineEnd = ctx.source.indexOf('\n', offset);
330
+ const line = ctx.source.slice(offset, lineEnd === -1 ? undefined : lineEnd);
331
+ const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
332
+ if (match) {
333
+ lexical.$setState(code, codeFenceState, match[1]);
334
+ }
335
+ }
336
+ // CodeNode only models the language; keep the rest of the info string
337
+ // (` ```js title=x `) as node state so it survives the round-trip.
338
+ if (node.meta) {
339
+ lexical.$setState(code, codeMetaState, node.meta);
340
+ }
341
+ if (node.value) {
342
+ $append(code, [lexical.$createTextNode(node.value)]);
343
+ }
344
+ return code;
345
+ };
346
+ const importText = (node, ctx) => ctx.createText(node.value);
347
+ const importHtml = (node, ctx) => ctx.createText(node.value);
348
+ const importInlineCode = (node, ctx) => ctx.createText(node.value, ctx.format | FORMAT_CODE);
349
+
350
+ /**
351
+ * Builds the emphasis/strong import handler: applies the format bit and
352
+ * records an underscore delimiter (`_em_` / `__b__`) on the resulting text
353
+ * nodes; `*` is the default and isn't stored.
354
+ */
355
+ function makeEmphasisImporter(format, markerState) {
356
+ return (node, ctx) => {
357
+ const children = ctx.importChildren(node, format);
358
+ if (inlineMarker(ctx, node) === '_') {
359
+ for (const child of children) {
360
+ if (lexical.$isTextNode(child)) {
361
+ lexical.$setState(child, markerState, '_');
362
+ }
363
+ }
364
+ }
365
+ return children;
366
+ };
367
+ }
368
+ const $importEmphasis = /* @__PURE__ */makeEmphasisImporter(FORMAT_ITALIC, emphasisMarkerState);
369
+ const $importStrong = /* @__PURE__ */makeEmphasisImporter(FORMAT_BOLD, strongMarkerState);
370
+ const importDelete = (node, ctx) => ctx.importChildren(node, FORMAT_STRIKETHROUGH);
371
+ const $importBreak = (node, ctx) => {
372
+ // An mdast `break` is always a HARD break; preserve whether it was written
373
+ // as `\` or as trailing spaces, defaulting to `\` when the literal cannot
374
+ // be recovered. (Soft breaks are newlines inside text values and stay
375
+ // unmarked, serializing back to a plain newline.)
376
+ let marker = '\\';
377
+ if (ctx.source && node.position) {
378
+ const {
379
+ start,
380
+ end
381
+ } = node.position;
382
+ if (start.offset != null && end.offset != null) {
383
+ const raw = ctx.source.slice(start.offset, end.offset).replace(/\n$/, '');
384
+ if (/^ {2,}$/.test(raw)) {
385
+ marker = raw;
386
+ }
387
+ }
388
+ }
389
+ return [lexical.$setState(lexical.$createLineBreakNode(), hardLineBreakState, marker)];
390
+ };
391
+ const $importLink = (node, ctx) => {
392
+ const link$1 = $append(link.$createLinkNode(node.url, {
393
+ title: node.title == null ? undefined : node.title
394
+ }), ctx.importChildren(node));
395
+ // Preserve the syntax the link was written in (`[text](url)` vs `<url>`
396
+ // vs a bare GFM autolink literal) so it round-trips unchanged.
397
+ if (ctx.source && node.position && node.position.start.offset != null) {
398
+ const first = ctx.source[node.position.start.offset];
399
+ lexical.$setState(link$1, linkStyleState, first === '[' ? 'inline' : first === '<' ? 'autolink' : 'literal');
400
+ }
401
+ return link$1;
402
+ };
403
+
404
+ /**
405
+ * CommonMark reference links (`[text][id]`, `[id][]`, `[id]`) resolve against
406
+ * the document's definitions. An unresolved reference is literal text per the
407
+ * spec, so it is re-emitted verbatim.
408
+ */
409
+ const $importLinkReference = (node, ctx) => {
410
+ const definition = ctx.getDefinition(node.identifier);
411
+ if (definition) {
412
+ return $append(link.$createLinkNode(definition.url, {
413
+ title: definition.title == null ? undefined : definition.title
414
+ }), ctx.importChildren(node));
415
+ }
416
+ const {
417
+ position
418
+ } = node;
419
+ if (ctx.source && position && position.start.offset != null) {
420
+ return ctx.createText(ctx.source.slice(position.start.offset, position.end.offset));
421
+ }
422
+ // Approximate the literal when the source is unavailable.
423
+ return [...ctx.createText('['), ...ctx.importChildren(node), ...ctx.createText(']')];
424
+ };
425
+
426
+ /**
427
+ * Definitions (`[id]: url "title"`) are consumed by {@link collectDefinitions}
428
+ * before the walk; the node itself produces no content.
429
+ */
430
+ const importDefinition = () => [];
431
+
432
+ /* -------------------------------------------------------------------------- *
433
+ * Export handlers: Lexical node -> mdast node(s) *
434
+ * -------------------------------------------------------------------------- */
435
+
436
+ const exportParagraph = (node, ctx) => {
437
+ if (!lexical.$isParagraphNode(node)) {
438
+ return null;
439
+ }
440
+ return {
441
+ children: ctx.exportInline(node),
442
+ type: 'paragraph'
443
+ };
444
+ };
445
+ const HEADING_DEPTHS = {
446
+ h1: 1,
447
+ h2: 2,
448
+ h3: 3,
449
+ h4: 4,
450
+ h5: 5,
451
+ h6: 6
452
+ };
453
+ const $exportHeading = (node, ctx) => {
454
+ if (!richText.$isHeadingNode(node)) {
455
+ return null;
456
+ }
457
+ const heading = {
458
+ children: ctx.exportInline(node),
459
+ depth: HEADING_DEPTHS[node.getTag()],
460
+ type: 'heading'
461
+ };
462
+ // Only pin the style when it is known; nodes created in the editor defer
463
+ // to the document-level serialization options.
464
+ if (lexical.$getState(node, setextState)) {
465
+ heading.data = {
466
+ mdastSetext: true
467
+ };
468
+ }
469
+ return heading;
470
+ };
471
+ const exportQuote = (node, ctx) => {
472
+ if (!richText.$isQuoteNode(node)) {
473
+ return null;
474
+ }
475
+ // A shadow root quote holds block-level children that dispatch directly;
476
+ // a legacy quote holds inline content that must be reassembled into
477
+ // paragraphs. Branching per node lets both forms coexist in one document.
478
+ // Children of a shadow root quote dispatch through the registry, which
479
+ // erases types; block-level output is the dispatch contract here.
480
+ return {
481
+ children: node.isShadowRoot() ? ctx.exportChildren(node) : ctx.exportBlocks(node),
482
+ type: 'blockquote'
483
+ };
484
+ };
485
+ const $exportCode = node => {
486
+ if (!codeCore.$isCodeNode(node)) {
487
+ return null;
488
+ }
489
+ const code = {
490
+ lang: node.getLanguage() || null,
491
+ type: 'code',
492
+ value: node.getTextContent()
493
+ };
494
+ // The info-string tail is a first-class mdast field; to-markdown emits it
495
+ // after the language (and forces fenced style).
496
+ const meta = lexical.$getState(node, codeMetaState);
497
+ if (meta) {
498
+ code.meta = meta;
499
+ }
500
+ // `data.mdastFence` is read back by the exporter's to-markdown wrapper.
501
+ // Only pin the fence when known; editor-created code blocks defer to the
502
+ // document-level serialization options.
503
+ const fence = lexical.$getState(node, codeFenceState);
504
+ if (fence) {
505
+ code.data = {
506
+ mdastFence: fence
507
+ };
508
+ }
509
+ return code;
510
+ };
511
+ const $exportLink = (node, ctx) => {
512
+ if (!link.$isLinkNode(node) || link.$isAutoLinkNode(node)) {
513
+ return null;
514
+ }
515
+ const link$1 = {
516
+ children: ctx.exportInline(node),
517
+ title: node.getTitle() ?? null,
518
+ type: 'link',
519
+ url: node.getURL()
520
+ };
521
+ // Only pinned when known — editor-created links defer to the default
522
+ // serialization (autolink form when the text is the URL).
523
+ const style = lexical.$getState(node, linkStyleState);
524
+ if (style) {
525
+ link$1.data = {
526
+ mdastLinkStyle: style
527
+ };
528
+ }
529
+ return link$1;
530
+ };
531
+ function $exportListNode(node, ctx) {
532
+ const listType = node.getListType();
533
+ const list$1 = {
534
+ children: [],
535
+ ordered: listType === 'number',
536
+ spread: false,
537
+ start: listType === 'number' ? node.getStart() : undefined,
538
+ type: 'list'
539
+ };
540
+ // Preserve the marker the list used; read back by the exporter's
541
+ // to-markdown wrapper. Only pinned when known — editor-created lists defer
542
+ // to the document-level serialization options.
543
+ if (listType === 'number') {
544
+ const marker = lexical.$getState(node, orderedMarkerState);
545
+ if (marker) {
546
+ list$1.data = {
547
+ mdastBulletOrdered: marker
548
+ };
549
+ }
550
+ } else {
551
+ const marker = lexical.$getState(node, listMarkerState);
552
+ if (marker) {
553
+ list$1.data = {
554
+ mdastBullet: marker
555
+ };
556
+ }
557
+ }
558
+ let previousItem = null;
559
+ for (const child of node.getChildren()) {
560
+ // Structural iteration bypasses the walk's selection filter, so items a
561
+ // selection export does not reach are skipped here.
562
+ if (!list.$isListItemNode(child) || !ctx.isIncluded(child)) {
563
+ continue;
564
+ }
565
+ const firstChild = child.getFirstChild();
566
+ // A list item whose sole child is a nested list represents nesting: attach
567
+ // the nested list to the previous item's children.
568
+ if (child.getChildrenSize() === 1 && list.$isListNode(firstChild)) {
569
+ const nested = $exportListNode(firstChild, ctx);
570
+ if (previousItem) {
571
+ previousItem.children.push(nested);
572
+ } else {
573
+ list$1.children.push({
574
+ children: [nested],
575
+ spread: false,
576
+ type: 'listItem'
577
+ });
578
+ }
579
+ continue;
580
+ }
581
+ const item = {
582
+ checked: listType === 'check' ? child.getChecked() ?? false : null,
583
+ children: ctx.exportBlocks(child),
584
+ spread: false,
585
+ type: 'listItem'
586
+ };
587
+ list$1.children.push(item);
588
+ previousItem = item;
589
+ }
590
+ return list$1;
591
+ }
592
+ const $exportList = (node, ctx) => {
593
+ if (!list.$isListNode(node)) {
594
+ return null;
595
+ }
596
+ return $exportListNode(node, ctx);
597
+ };
598
+
599
+ /**
600
+ * Wraps a plain string in the mdast phrasing nodes implied by a Lexical text
601
+ * format bitmask (code span innermost, then emphasis, strong, strikethrough).
602
+ * The emphasis/strong *delimiter* (`*` vs `_`) is a document-level to-markdown
603
+ * option (see the exporter) rather than per-node, because mixing delimiters in
604
+ * one document desyncs to-markdown's character escaping.
605
+ */
606
+ function phrasingFromFormattedText(value, format) {
607
+ let content = format & FORMAT_CODE ? {
608
+ type: 'inlineCode',
609
+ value
610
+ } : {
611
+ type: 'text',
612
+ value
613
+ };
614
+ if (format & FORMAT_ITALIC) {
615
+ content = {
616
+ children: [content],
617
+ type: 'emphasis'
618
+ };
619
+ }
620
+ if (format & FORMAT_BOLD) {
621
+ content = {
622
+ children: [content],
623
+ type: 'strong'
624
+ };
625
+ }
626
+ if (format & FORMAT_STRIKETHROUGH) {
627
+ content = {
628
+ children: [content],
629
+ type: 'delete'
630
+ };
631
+ }
632
+ return content;
633
+ }
634
+ const exportText = node => {
635
+ if (!lexical.$isTextNode(node)) {
636
+ return null;
637
+ }
638
+ return phrasingFromFormattedText(node.getTextContent(), node.getFormat() & TEXT_FORMAT_MASK);
639
+ };
640
+ const $exportLineBreak = node => lexical.$isLineBreakNode(node) ? {
641
+ data: {
642
+ mdastBreak: lexical.$getState(node, hardLineBreakState)
643
+ },
644
+ type: 'break'
645
+ } : null;
646
+ const exportTab = node => lexical.$isTabNode(node) ? {
647
+ type: 'text',
648
+ value: '\t'
649
+ } : null;
650
+
651
+ /**
652
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
653
+ *
654
+ * This source code is licensed under the MIT license found in the
655
+ * LICENSE file in the root directory of this source tree.
656
+ *
657
+ */
658
+
659
+
660
+ /**
661
+ * Runs `fn` with `state.options` temporarily overridden, restoring the
662
+ * previous values afterwards so a per-node override never leaks into the rest
663
+ * of the serialization.
664
+ */
665
+ function withOptions(state, overrides, fn) {
666
+ const {
667
+ options
668
+ } = state;
669
+ const saved = {};
670
+ for (const key of Object.keys(overrides)) {
671
+ saved[key] = options[key];
672
+ options[key] = overrides[key];
673
+ }
674
+ try {
675
+ return fn();
676
+ } finally {
677
+ for (const key of Object.keys(saved)) {
678
+ options[key] = saved[key];
679
+ }
680
+ }
681
+ }
682
+
683
+ /**
684
+ * A to-markdown extension whose handlers reproduce the literal Markdown syntax
685
+ * captured on import (and stored on the Lexical nodes), by temporarily
686
+ * steering the default handlers with per-node options. Delegating to the
687
+ * defaults keeps all the indentation / nesting / disambiguation behavior
688
+ * intact while still honoring each node's original marker/fence. Nodes without
689
+ * captured syntax fall straight through to the defaults, so document-level
690
+ * options (and contributed `toMarkdownExtensions`) still apply to them.
691
+ */
692
+ const SYNTAX_TO_MARKDOWN = {
693
+ handlers: {
694
+ break(node, parent, state, info) {
695
+ // Delegate first: the default handler substitutes a space when a real
696
+ // EOL is unsafe in the current construct (headings, table cells). Only
697
+ // when it chose the hard-break form does the preserved marker apply:
698
+ // trailing-space markers are reproduced, and the empty marker means the
699
+ // break is SOFT (a source newline or editor line break) and serializes
700
+ // as a plain newline rather than being upgraded to a hard break.
701
+ const result = mdastUtilToMarkdown.defaultHandlers.break(node, parent, state, info);
702
+ if (result !== '\\\n') {
703
+ return result;
704
+ }
705
+ const marker = node.data && node.data.mdastBreak;
706
+ if (!marker) {
707
+ return '\n';
708
+ }
709
+ return /^ {2,}$/.test(marker) ? `${marker}\n` : result;
710
+ },
711
+ code(node, parent, state, info) {
712
+ const fence = node.data && node.data.mdastFence;
713
+ if (!fence) {
714
+ return mdastUtilToMarkdown.defaultHandlers.code(node, parent, state, info);
715
+ }
716
+ return withOptions(state, {
717
+ fence: fence[0] === '~' ? '~' : '`',
718
+ fences: true
719
+ }, () => mdastUtilToMarkdown.defaultHandlers.code(node, parent, state, info));
720
+ },
721
+ heading(node, parent, state, info) {
722
+ // `data.mdastSetext` is only present (true) for imported setext
723
+ // headings; everything else defers to the document-level option.
724
+ if (!(node.data && node.data.mdastSetext === true)) {
725
+ return mdastUtilToMarkdown.defaultHandlers.heading(node, parent, state, info);
726
+ }
727
+ return withOptions(state, {
728
+ setext: true
729
+ }, () => mdastUtilToMarkdown.defaultHandlers.heading(node, parent, state, info));
730
+ },
731
+ link(node, parent, state, info) {
732
+ // `data.mdastLinkStyle` preserves the syntax the link was written in.
733
+ // A title can't be expressed in autolink/literal form, so links with a
734
+ // title always fall through to the default (resource form).
735
+ const style = node.data && node.data.mdastLinkStyle;
736
+ if (style === 'literal' && node.title == null) {
737
+ // A bare GFM autolink literal: emit the visible text as-is. The
738
+ // gfm-autolink-literal to-markdown extension keeps surrounding
739
+ // escaping consistent so it re-parses as the same literal.
740
+ return mdastUtilToString.toString(node);
741
+ }
742
+ if (style === 'inline') {
743
+ // Force `[text](url)` even when the text equals the URL (the
744
+ // default handler would normalize that to an autolink).
745
+ return withOptions(state, {
746
+ resourceLink: true
747
+ }, () => mdastUtilToMarkdown.defaultHandlers.link(node, parent, state, info));
748
+ }
749
+ return mdastUtilToMarkdown.defaultHandlers.link(node, parent, state, info);
750
+ },
751
+ list(node, parent, state, info) {
752
+ if (node.ordered) {
753
+ const ordered = node.data && node.data.mdastBulletOrdered;
754
+ if (ordered == null) {
755
+ return mdastUtilToMarkdown.defaultHandlers.list(node, parent, state, info);
756
+ }
757
+ return withOptions(state, {
758
+ bulletOrdered: ordered
759
+ }, () => mdastUtilToMarkdown.defaultHandlers.list(node, parent, state, info));
760
+ }
761
+ const bullet = node.data && node.data.mdastBullet;
762
+ if (bullet == null) {
763
+ return mdastUtilToMarkdown.defaultHandlers.list(node, parent, state, info);
764
+ }
765
+ return withOptions(state, {
766
+ bullet,
767
+ bulletOther: bullet === '-' ? '*' : '-'
768
+ }, () => mdastUtilToMarkdown.defaultHandlers.list(node, parent, state, info));
769
+ },
770
+ thematicBreak(node, parent, state) {
771
+ const marker = node.data && node.data.mdastRule;
772
+ if (marker !== '-' && marker !== '*' && marker !== '_') {
773
+ return mdastUtilToMarkdown.defaultHandlers.thematicBreak(node, parent, state);
774
+ }
775
+ return withOptions(state, {
776
+ rule: marker
777
+ }, () => mdastUtilToMarkdown.defaultHandlers.thematicBreak(node, parent, state));
778
+ }
779
+ }
780
+ };
781
+
782
+ /**
783
+ * Accumulates adjacent plain text nodes that share a format so they serialize
784
+ * to a single delimiter pair (e.g. `**ab**` rather than `**a****b**`). Shared
785
+ * by the inline and block export walks.
786
+ */
787
+ class TextRunAccumulator {
788
+ format = -1;
789
+ value = '';
790
+
791
+ /**
792
+ * Returns `true` when `child` was absorbed. A format change flushes the
793
+ * previous run into `out` first. The caller decides *which* text nodes
794
+ * are eligible (plain text with no export rule of its own); this only
795
+ * guards the node kind.
796
+ */
797
+ push(child, out) {
798
+ if (!lexical.$isTextNode(child)) {
799
+ return false;
800
+ }
801
+ const format = child.getFormat() & TEXT_FORMAT_MASK;
802
+ if (format === this.format) {
803
+ this.value += child.getTextContent();
804
+ } else {
805
+ this.flushInto(out);
806
+ this.format = format;
807
+ this.value = child.getTextContent();
808
+ }
809
+ return true;
810
+ }
811
+ flushInto(out) {
812
+ if (this.format >= 0) {
813
+ out.push(phrasingFromFormattedText(this.value, this.format));
814
+ }
815
+ this.format = -1;
816
+ this.value = '';
817
+ }
818
+ }
819
+ function createNodeExporter(compiled, selection$1 = null) {
820
+ const {
821
+ exportHandlers
822
+ } = compiled;
823
+ // Incremented whenever a selected leaf contributes output. Parents compare
824
+ // it before/after recursing to keep an element that is not itself selected
825
+ // but hosts selected content (the markdown analogue of extractWithChild).
826
+ let selectionHits = 0;
827
+
828
+ /**
829
+ * Whether the run accumulator may absorb `child`: a text node whose only
830
+ * export behavior would be the core text fallback. A node whose type has
831
+ * its own registered rule (or the core `'text'` rule, which the
832
+ * accumulator supersedes to merge adjacent runs) must not be swallowed —
833
+ * this keeps replaced/custom text nodes dispatching to their handlers.
834
+ */
835
+ function mayAccumulate(child) {
836
+ const handler = exportHandlers.get(child.getType());
837
+ return handler === undefined || handler === exportText;
838
+ }
839
+
840
+ /**
841
+ * Selection filter for one child. With no selection every child passes
842
+ * through unchanged. Leaves (text, line break, decorator) pass only when
843
+ * selected, a partially selected text node as a detached clone sliced to
844
+ * the selected range; `null` means skip. Elements always pass — they are
845
+ * judged after recursion by {@link $dispatchElement}.
846
+ */
847
+ function $filterChild(child) {
848
+ if (selection$1 === null || lexical.$isElementNode(child)) {
849
+ return child;
850
+ }
851
+ if (!child.isSelected(selection$1)) {
852
+ return null;
853
+ }
854
+ selectionHits++;
855
+ return lexical.$isTextNode(child) ? selection.$sliceSelectedTextNodeContent(selection$1, child, 'clone') : child;
856
+ }
857
+
858
+ /**
859
+ * Whether `node` or any descendant is selected. See
860
+ * {@link MdastExportContext.isIncluded}.
861
+ */
862
+ function $isIncluded(node) {
863
+ if (selection$1 === null || node.isSelected(selection$1)) {
864
+ return true;
865
+ }
866
+ if (lexical.$isElementNode(node)) {
867
+ for (const child of node.getChildren()) {
868
+ if ($isIncluded(child)) {
869
+ return true;
870
+ }
871
+ }
872
+ }
873
+ return false;
874
+ }
875
+
876
+ /**
877
+ * Dispatches an element child, appending its output to `out` unless a
878
+ * selection is active and neither the element nor any descendant is
879
+ * selected.
880
+ */
881
+ function $dispatchElement(child, out) {
882
+ const selfSelected = selection$1 !== null && child.isSelected(selection$1);
883
+ if (selfSelected) {
884
+ selectionHits++;
885
+ }
886
+ const before = selectionHits;
887
+ const result = $dispatch(child);
888
+ if (selection$1 === null || selfSelected || selectionHits > before) {
889
+ out.push(...result);
890
+ }
891
+ }
892
+ const context = {
893
+ exportBlocks: node => $exportBlocks(node),
894
+ exportChildren: node => {
895
+ const out = [];
896
+ for (const child of node.getChildren()) {
897
+ const target = $filterChild(child);
898
+ if (target === null) {
899
+ continue;
900
+ }
901
+ if (lexical.$isElementNode(target)) {
902
+ $dispatchElement(target, out);
903
+ } else {
904
+ out.push(...$dispatch(target));
905
+ }
906
+ }
907
+ return out;
908
+ },
909
+ exportInline: node => $exportInline(node),
910
+ isIncluded: $isIncluded
911
+ };
912
+ function $dispatch(node) {
913
+ const handler = exportHandlers.get(node.getType());
914
+ if (handler) {
915
+ const result = handler(node, context);
916
+ if (result != null) {
917
+ return Array.isArray(result) ? result : [result];
918
+ }
919
+ }
920
+ // Fallbacks keep unknown nodes from disappearing entirely; text and line
921
+ // breaks reuse the core handlers so their behavior can't drift.
922
+ const asText = exportText(node);
923
+ if (asText !== null) {
924
+ return [asText];
925
+ }
926
+ const asBreak = $exportLineBreak(node);
927
+ if (asBreak !== null) {
928
+ return [asBreak];
929
+ }
930
+ if (lexical.$isElementNode(node)) {
931
+ return context.exportChildren(node);
932
+ }
933
+ const text = node.getTextContent();
934
+ return text ? [{
935
+ type: 'text',
936
+ value: text
937
+ }] : [];
938
+ }
939
+
940
+ /**
941
+ * Converts the inline children of `node` into phrasing content.
942
+ */
943
+ function $exportInline(node) {
944
+ const result = [];
945
+ const runs = new TextRunAccumulator();
946
+ for (const child of node.getChildren()) {
947
+ const target = $filterChild(child);
948
+ if (target === null) {
949
+ continue;
950
+ }
951
+ if (!(mayAccumulate(target) && runs.push(target, result))) {
952
+ runs.flushInto(result);
953
+ if (lexical.$isElementNode(target)) {
954
+ // The registry erases types; phrasing output is the dispatch
955
+ // contract for inline children.
956
+ $dispatchElement(target, result);
957
+ } else {
958
+ result.push(...$dispatch(target));
959
+ }
960
+ }
961
+ }
962
+ runs.flushInto(result);
963
+ return result;
964
+ }
965
+
966
+ /**
967
+ * Converts a container whose Lexical children are inline (block quote, list
968
+ * item) into mdast block content. A LineBreakNode marked as a paragraph
969
+ * boundary (set by the import handlers when joining sibling paragraphs)
970
+ * splits the content; any other LineBreakNode stays an inline `break`
971
+ * (hard or soft according to its marker). Nested block children pass
972
+ * through directly.
973
+ */
974
+ function $exportBlocks(node) {
975
+ const blocks = [];
976
+ let inline = [];
977
+ const runs = new TextRunAccumulator();
978
+ const flushParagraph = () => {
979
+ runs.flushInto(inline);
980
+ if (inline.length > 0) {
981
+ blocks.push({
982
+ children: inline,
983
+ type: 'paragraph'
984
+ });
985
+ inline = [];
986
+ }
987
+ };
988
+ for (const child of node.getChildren()) {
989
+ const target = $filterChild(child);
990
+ if (target === null) {
991
+ continue;
992
+ }
993
+ if (lexical.$isLineBreakNode(target)) {
994
+ const asBreak = $exportLineBreak(target);
995
+ if (lexical.$getState(target, paragraphBreakState) || asBreak === null) {
996
+ flushParagraph();
997
+ } else {
998
+ runs.flushInto(inline);
999
+ inline.push(asBreak);
1000
+ }
1001
+ } else if (mayAccumulate(target) && runs.push(target, inline)) {
1002
+ continue;
1003
+ } else if ($isBlockLevelNode(target)) {
1004
+ flushParagraph();
1005
+ if (lexical.$isElementNode(target)) {
1006
+ // The registry erases types; block-level output is the dispatch
1007
+ // contract for block children.
1008
+ $dispatchElement(target, blocks);
1009
+ } else {
1010
+ blocks.push(...$dispatch(target));
1011
+ }
1012
+ } else if (lexical.$isElementNode(target)) {
1013
+ runs.flushInto(inline);
1014
+ $dispatchElement(target, inline);
1015
+ } else {
1016
+ runs.flushInto(inline);
1017
+ inline.push(...$dispatch(target));
1018
+ }
1019
+ }
1020
+ flushParagraph();
1021
+ if (blocks.length === 0) {
1022
+ blocks.push({
1023
+ children: [],
1024
+ type: 'paragraph'
1025
+ });
1026
+ }
1027
+ return blocks;
1028
+ }
1029
+ return {
1030
+ exportChildren: context.exportChildren
1031
+ };
1032
+ }
1033
+
1034
+ /**
1035
+ * Picks the document-level emphasis and strong delimiters from the first
1036
+ * italic / bold text node that recorded a known delimiter on import, scanning
1037
+ * the tree rooted at `node`. Mixing delimiters within one document is not
1038
+ * supported by to-markdown's escaping, so a single choice is made per
1039
+ * document; nodes with no recorded delimiter (created in the editor) are
1040
+ * skipped so they cannot mask the document's authored style.
1041
+ */
1042
+ function $dominantInlineMarkers(node) {
1043
+ let emphasis;
1044
+ let strong;
1045
+ const visit = element => {
1046
+ for (const child of element.getChildren()) {
1047
+ if (lexical.$isTextNode(child)) {
1048
+ if (emphasis === undefined && child.hasFormat('italic')) {
1049
+ const marker = lexical.$getState(child, emphasisMarkerState);
1050
+ if (marker === '_') {
1051
+ emphasis = '_';
1052
+ }
1053
+ }
1054
+ if (strong === undefined && child.hasFormat('bold')) {
1055
+ const marker = lexical.$getState(child, strongMarkerState);
1056
+ if (marker === '_') {
1057
+ strong = '_';
1058
+ }
1059
+ }
1060
+ } else if (lexical.$isElementNode(child)) {
1061
+ visit(child);
1062
+ }
1063
+ if (emphasis !== undefined && strong !== undefined) {
1064
+ return;
1065
+ }
1066
+ }
1067
+ };
1068
+ visit(node);
1069
+ return {
1070
+ emphasis,
1071
+ strong
1072
+ };
1073
+ }
1074
+
1075
+ /**
1076
+ * Creates a reusable exporter that converts the Lexical tree rooted at the
1077
+ * supplied element (or the editor root) — or just the selected content —
1078
+ * into a Markdown string.
1079
+ */
1080
+ function createMdastExport(compiled) {
1081
+ // The unfiltered exporter has no per-call state and is shared by every
1082
+ // whole-document export; selection exports build a fresh one per call.
1083
+ const documentExporter = createNodeExporter(compiled);
1084
+ const $toMdast = (root, exporter) => ({
1085
+ // The registry erases types; root-level output is the dispatch contract
1086
+ // for top-level children.
1087
+ children: exporter.exportChildren(root),
1088
+ type: 'root'
1089
+ });
1090
+ const $serialize = (tree, root) => {
1091
+ // Emphasis/strong delimiters are document-level; the delimiter recorded on
1092
+ // import wins, otherwise contributed toMarkdownExtensions (and the '-'
1093
+ // bullet baseline) decide. Defaults ride as the FIRST extension so that
1094
+ // contributed extensions can override them; SYNTAX_TO_MARKDOWN runs last
1095
+ // so its handlers reproduce the per-node syntax captured on import.
1096
+ const {
1097
+ emphasis,
1098
+ strong
1099
+ } = $dominantInlineMarkers(root);
1100
+ const defaults = {
1101
+ bullet: '-'
1102
+ };
1103
+ if (emphasis) {
1104
+ defaults.emphasis = emphasis;
1105
+ }
1106
+ if (strong) {
1107
+ defaults.strong = strong;
1108
+ }
1109
+ const out = mdastUtilToMarkdown.toMarkdown(tree, {
1110
+ extensions: [defaults, ...compiled.toMarkdownExtensions, SYNTAX_TO_MARKDOWN]
1111
+ });
1112
+ // toMarkdown always appends a trailing newline; drop it so callers get the
1113
+ // same shape as `@lexical/markdown`'s `$convertToMarkdownString`.
1114
+ return out.replace(/\n$/, '');
1115
+ };
1116
+ const $exportToMdast = node => $toMdast(node || lexical.$getRoot(), documentExporter);
1117
+ const $exportToMarkdown = node => {
1118
+ const root = node || lexical.$getRoot();
1119
+ return $serialize($toMdast(root, documentExporter), root);
1120
+ };
1121
+ const $exportSelectionToMarkdown = (selection = lexical.$getSelection()) => {
1122
+ if (selection === null || lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
1123
+ return '';
1124
+ }
1125
+ const root = lexical.$getRoot();
1126
+ return $serialize($toMdast(root, createNodeExporter(compiled, selection)), root);
1127
+ };
1128
+ return {
1129
+ $exportSelectionToMarkdown,
1130
+ $exportToMarkdown,
1131
+ $exportToMdast
1132
+ };
1133
+ }
1134
+
1135
+ /**
1136
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1137
+ *
1138
+ * This source code is licensed under the MIT license found in the
1139
+ * LICENSE file in the root directory of this source tree.
1140
+ *
1141
+ */
1142
+
1143
+ /**
1144
+ * Compiles the raw contribution arrays held in {@link MdastConfig} into the
1145
+ * indexed registry used at runtime. Rules earlier in the arrays win for a
1146
+ * given node `type`; since {@link MdastImportExtension}'s `mergeConfig` prepends the
1147
+ * rules contributed by extensions merged later (closer to the editor root),
1148
+ * those higher-priority rules take precedence — mirroring the dispatch order
1149
+ * of `@lexical/html`'s `DOMImportExtension`.
1150
+ */
1151
+ function compileMdast(config) {
1152
+ const importHandlers = new Map();
1153
+ const exportHandlers = new Map();
1154
+ for (const rule of config.importRules) {
1155
+ if (!importHandlers.has(rule.type)) {
1156
+ importHandlers.set(rule.type, rule.$import);
1157
+ }
1158
+ }
1159
+ for (const rule of config.exportRules) {
1160
+ if (!exportHandlers.has(rule.type)) {
1161
+ exportHandlers.set(rule.type, rule.$export);
1162
+ }
1163
+ }
1164
+ return {
1165
+ exportHandlers,
1166
+ importHandlers,
1167
+ inlineShortcutTriggers: new Set(config.inlineShortcutTriggers),
1168
+ inlineShortcutTypes: new Set(config.inlineShortcutTypes),
1169
+ mdastExtensions: [...config.mdastExtensions],
1170
+ micromarkExtensions: [...config.micromarkExtensions],
1171
+ toMarkdownExtensions: [...config.toMarkdownExtensions]
1172
+ };
1173
+ }
1174
+
1175
+ /**
1176
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1177
+ *
1178
+ * This source code is licensed under the MIT license found in the
1179
+ * LICENSE file in the root directory of this source tree.
1180
+ *
1181
+ */
1182
+
1183
+
1184
+ /**
1185
+ * Splits `value` into a run of `TextNode`s via {@link tokenizeRawText}:
1186
+ * `\n` becomes a `LineBreakNode` and `\t` a `TabNode` (matching how typed
1187
+ * content is represented in the editor), with `format` applied to each text
1188
+ * segment. Empty segments are dropped so a leading/trailing/standalone
1189
+ * separator yields only its node.
1190
+ */
1191
+ function $createTextNodes(value, format) {
1192
+ const out = [];
1193
+ lexical.tokenizeRawText(value, {
1194
+ linebreak: () => out.push(lexical.$createLineBreakNode()),
1195
+ tab: () => out.push(lexical.$createTabNode()),
1196
+ text: segment => {
1197
+ const textNode = lexical.$createTextNode(segment);
1198
+ if (format) {
1199
+ textNode.setFormat(format);
1200
+ }
1201
+ out.push(textNode);
1202
+ }
1203
+ });
1204
+ return out;
1205
+ }
1206
+
1207
+ /** A resolved `[identifier]: url "title"` definition. */
1208
+
1209
+ /**
1210
+ * Collects the document's definitions (`[id]: url "title"`) so link/image
1211
+ * references can be resolved during the import walk. Identifiers on mdast
1212
+ * `definition` nodes are already normalized.
1213
+ */
1214
+ function collectDefinitions(tree) {
1215
+ const definitions = new Map();
1216
+ const visit = node => {
1217
+ if (node.type === 'definition') {
1218
+ // CommonMark: the FIRST definition of an identifier wins.
1219
+ if (!definitions.has(node.identifier)) {
1220
+ definitions.set(node.identifier, {
1221
+ title: node.title,
1222
+ url: node.url
1223
+ });
1224
+ }
1225
+ }
1226
+ if ('children' in node) {
1227
+ for (const child of node.children) {
1228
+ visit(child);
1229
+ }
1230
+ }
1231
+ };
1232
+ visit(tree);
1233
+ return definitions;
1234
+ }
1235
+ const NO_DEFINITIONS = new Map();
1236
+
1237
+ /**
1238
+ * Builds the recursive importer for a compiled set of transformers. The
1239
+ * returned function converts a single mdast node into Lexical nodes, threading
1240
+ * the accumulated text-format bitmask through inline marks.
1241
+ *
1242
+ * Exported so the streaming shortcut engine can reuse the exact same mdast ->
1243
+ * Lexical mapping when materializing an inline construct it detected.
1244
+ */
1245
+ function createNodeImporter(compiled, source = '', definitions = NO_DEFINITIONS) {
1246
+ const {
1247
+ importHandlers
1248
+ } = compiled;
1249
+ // The context only depends on the accumulated format bitmask, which takes a
1250
+ // handful of distinct values per document — cache instead of allocating one
1251
+ // (plus three closures) per visited node.
1252
+ const contextByFormat = new Map();
1253
+ function getContext(format) {
1254
+ let context = contextByFormat.get(format);
1255
+ if (context === undefined) {
1256
+ context = {
1257
+ createText: (value, fmt) => $createTextNodes(value, fmt == null ? format : fmt),
1258
+ format,
1259
+ getDefinition: identifier => definitions.get(identifier),
1260
+ importChildren: (parent, extra) => $importChildren(parent, format | (extra || 0)),
1261
+ importNode: (node, extra) => $importNode(node, format | (extra || 0)),
1262
+ source
1263
+ };
1264
+ contextByFormat.set(format, context);
1265
+ }
1266
+ return context;
1267
+ }
1268
+ function $importNode(node, format) {
1269
+ const handler = importHandlers.get(node.type);
1270
+ if (handler) {
1271
+ const result = handler(node, getContext(format));
1272
+ if (result == null) {
1273
+ return [];
1274
+ }
1275
+ return Array.isArray(result) ? result : [result];
1276
+ }
1277
+ // Fallback: unwrap unknown containers, render unknown literals as text, and
1278
+ // drop anything else so no content silently corrupts the tree.
1279
+ if ('children' in node) {
1280
+ return $importChildren(node, format);
1281
+ }
1282
+ if ('value' in node && typeof node.value === 'string') {
1283
+ return $createTextNodes(node.value, format);
1284
+ }
1285
+ return [];
1286
+ }
1287
+ function $importChildren(parent, format) {
1288
+ const out = [];
1289
+ for (const child of parent.children) {
1290
+ out.push(...$importNode(child, format));
1291
+ }
1292
+ return out;
1293
+ }
1294
+ return {
1295
+ $importChildren,
1296
+ $importNode
1297
+ };
1298
+ }
1299
+
1300
+ /**
1301
+ * Creates the import entry points for a compiled registry. The `Markdown`
1302
+ * variants parse a source string (recovering literal syntax like the list
1303
+ * bullet or link style from it); the `Mdast` variants walk a pre-parsed
1304
+ * tree, where no source string exists so syntax-preservation is skipped.
1305
+ * `$generateNodesFrom*` return an array of detached block-level Lexical
1306
+ * nodes without touching the document or the selection; `$import*` replace
1307
+ * the contents of the root (or a supplied element) with that result.
1308
+ */
1309
+ function createMdastImport(compiled) {
1310
+ const $generateNodes = (tree, source) => {
1311
+ const {
1312
+ $importNode
1313
+ } = createNodeImporter(compiled, source, collectDefinitions(tree));
1314
+
1315
+ // Top-level mdast children should produce block-level Lexical nodes. Any
1316
+ // stray inline content (e.g. from a fallback) is wrapped in a paragraph
1317
+ // so the result only ever contains valid block children.
1318
+ const blocks = [];
1319
+ let pendingParagraph = null;
1320
+ const flushPending = () => {
1321
+ if (pendingParagraph) {
1322
+ blocks.push(pendingParagraph);
1323
+ pendingParagraph = null;
1324
+ }
1325
+ };
1326
+ for (const child of tree.children) {
1327
+ for (const lexicalNode of $importNode(child, 0)) {
1328
+ if ($isBlockLevelNode(lexicalNode)) {
1329
+ flushPending();
1330
+ blocks.push(lexicalNode);
1331
+ } else {
1332
+ if (!pendingParagraph) {
1333
+ pendingParagraph = lexical.$createParagraphNode();
1334
+ }
1335
+ $append(pendingParagraph, [lexicalNode]);
1336
+ }
1337
+ }
1338
+ }
1339
+ flushPending();
1340
+ return blocks;
1341
+ };
1342
+ const $generateNodesFromMarkdown = markdown => $generateNodes(mdastUtilFromMarkdown.fromMarkdown(markdown, {
1343
+ extensions: compiled.micromarkExtensions,
1344
+ mdastExtensions: compiled.mdastExtensions
1345
+ }), markdown);
1346
+ const $generateNodesFromMdast = tree => $generateNodes(tree, '');
1347
+ const $replaceWithBlocks = (blocks, node) => {
1348
+ const root = node || lexical.$getRoot();
1349
+ root.clear();
1350
+ $prepend(root, blocks.length > 0 ? blocks : [lexical.$createParagraphNode()]);
1351
+ if (lexical.$getSelection() !== null) {
1352
+ root.selectStart();
1353
+ }
1354
+ };
1355
+ return {
1356
+ $generateNodesFromMarkdown,
1357
+ $generateNodesFromMdast,
1358
+ $importMarkdown: (markdown, node) => $replaceWithBlocks($generateNodesFromMarkdown(markdown), node),
1359
+ $importMdast: (tree, node) => $replaceWithBlocks($generateNodesFromMdast(tree), node)
1360
+ };
1361
+ }
1362
+
1363
+ /**
1364
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1365
+ *
1366
+ * This source code is licensed under the MIT license found in the
1367
+ * LICENSE file in the root directory of this source tree.
1368
+ *
1369
+ */
1370
+
1371
+ /**
1372
+ * Returns the offset of the first content character inside a block construct,
1373
+ * i.e. the length of the leading block marker (`"## "`, `"- "`, `"> "`, ...).
1374
+ * When the construct has no content yet (the user has only typed the marker)
1375
+ * the whole `line` is the marker.
1376
+ *
1377
+ * The walk descends through *container* levels only (blockquote -> paragraph,
1378
+ * list -> listItem -> paragraph) and stops at the first inline child of a
1379
+ * paragraph or heading — descending further would treat inline delimiters
1380
+ * (`**`, `[`, `` ` ``) as part of the block marker.
1381
+ */
1382
+ function contentStartOffset(node, line) {
1383
+ let current = node;
1384
+ for (;;) {
1385
+ if (current.type === 'heading' || current.type === 'paragraph') {
1386
+ const first = current.children[0];
1387
+ return first && first.position && first.position.start.offset != null ? first.position.start.offset : line.length;
1388
+ }
1389
+ if (!('children' in current) || current.children.length === 0) {
1390
+ return line.length;
1391
+ }
1392
+ current = current.children[0];
1393
+ }
1394
+ }
1395
+
1396
+ /**
1397
+ * `MarkdownStreamScanner` is the streaming heart of the shortcut engine. Each
1398
+ * keystroke feeds the growing line/inline buffer back through micromark (via
1399
+ * `mdast-util-from-markdown`), so shortcut recognition uses the *exact* same
1400
+ * grammar — and the same enabled extensions — as full-document import. There
1401
+ * is no second, divergent set of regular expressions to keep in sync.
1402
+ *
1403
+ * It is constructed from the {@link CompiledMdast} registry assembled by
1404
+ * {@link MdastImportExtension}, so it stays in lock-step with whatever feature
1405
+ * extensions are enabled — including the inline construct types and trigger
1406
+ * characters contributed via `inlineShortcutTypes` / `inlineShortcutTriggers`.
1407
+ */
1408
+ class MarkdownStreamScanner {
1409
+ compiled;
1410
+ importNode;
1411
+ /**
1412
+ * Whether the registry's grammar recognizes GFM task-list items (i.e.
1413
+ * `MdastTaskListExtension` contributed `gfmTaskListItem`). Probed by
1414
+ * parsing rather than configured, so it can never drift from the grammar.
1415
+ */
1416
+ supportsTaskListItems;
1417
+ constructor(compiled) {
1418
+ this.compiled = compiled;
1419
+ this.importNode = createNodeImporter(compiled, '').$importNode;
1420
+ const probe = this.parse('- [x] a').children[0];
1421
+ this.supportsTaskListItems = probe != null && probe.type === 'list' && probe.children[0].checked === true;
1422
+ }
1423
+
1424
+ /** Characters that can close an inline construct for this registry. */
1425
+ get inlineTriggers() {
1426
+ return this.compiled.inlineShortcutTriggers;
1427
+ }
1428
+ parse(value) {
1429
+ return mdastUtilFromMarkdown.fromMarkdown(value, {
1430
+ extensions: this.compiled.micromarkExtensions,
1431
+ mdastExtensions: this.compiled.mdastExtensions
1432
+ });
1433
+ }
1434
+
1435
+ /**
1436
+ * Materializes an mdast inline node into Lexical nodes using the same
1437
+ * import handlers as the full-document importer.
1438
+ */
1439
+ importInline(node) {
1440
+ return this.importNode(node, 0);
1441
+ }
1442
+
1443
+ /**
1444
+ * Recognizes a block-level construct at the start of `line`. Returns the
1445
+ * matched construct together with the marker length, or `null`.
1446
+ */
1447
+ scanBlock(line) {
1448
+ if (line.trim() === '') {
1449
+ return null;
1450
+ }
1451
+ const first = this.parse(line).children[0];
1452
+ // Only offer shortcuts for constructs the registry can actually import:
1453
+ // in a granular setup (e.g. no blockquote extension) the syntax should
1454
+ // stay literal rather than materialize an unregistered node.
1455
+ if (!first || !this.compiled.importHandlers.has(first.type)) {
1456
+ return null;
1457
+ }
1458
+ switch (first.type) {
1459
+ case 'heading':
1460
+ return {
1461
+ kind: 'heading',
1462
+ markerLength: contentStartOffset(first, line),
1463
+ node: first
1464
+ };
1465
+ case 'blockquote':
1466
+ return {
1467
+ kind: 'blockquote',
1468
+ markerLength: contentStartOffset(first, line),
1469
+ node: first
1470
+ };
1471
+ case 'list':
1472
+ return {
1473
+ kind: 'list',
1474
+ markerLength: contentStartOffset(first, line),
1475
+ node: first
1476
+ };
1477
+ case 'code':
1478
+ // A code construct recognized from a single line is just the opening
1479
+ // fence (possibly with a language); the entire line is the marker.
1480
+ return {
1481
+ kind: 'code',
1482
+ markerLength: line.length,
1483
+ node: first
1484
+ };
1485
+ default:
1486
+ return null;
1487
+ }
1488
+ }
1489
+
1490
+ /**
1491
+ * Recognizes an inline construct (emphasis, strong, strikethrough, inline
1492
+ * code, link, plus any registered `inlineShortcutTypes`) whose closing
1493
+ * delimiter falls exactly at the end of `value` (the text up to the caret).
1494
+ * Returns the mdast node, or `null`.
1495
+ */
1496
+ scanInline(value) {
1497
+ if (value.length === 0) {
1498
+ return null;
1499
+ }
1500
+ const tree = this.parse(value);
1501
+ const lastBlock = tree.children[tree.children.length - 1];
1502
+ if (!lastBlock || lastBlock.type !== 'paragraph') {
1503
+ return null;
1504
+ }
1505
+ const last = lastBlock.children[lastBlock.children.length - 1];
1506
+ if (!last || !last.position || last.position.end.offset !== value.length || !this.compiled.inlineShortcutTypes.has(last.type)) {
1507
+ return null;
1508
+ }
1509
+ // Guard against firing mid-delimiter: while typing `**bold*` the parser
1510
+ // briefly sees emphasis (`*bold*`) closing at the caret with a stray `*`
1511
+ // in front. If the opening delimiter is immediately preceded by the same
1512
+ // delimiter character, defer until the user finishes the longer run.
1513
+ const start = last.position.start.offset ?? 0;
1514
+ const openChar = value[start];
1515
+ if (start > 0 && value[start - 1] === openChar && this.compiled.inlineShortcutTriggers.has(openChar)) {
1516
+ return null;
1517
+ }
1518
+ return last;
1519
+ }
1520
+ }
1521
+
1522
+ /**
1523
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1524
+ *
1525
+ * This source code is licensed under the MIT license found in the
1526
+ * LICENSE file in the root directory of this source tree.
1527
+ *
1528
+ */
1529
+
1530
+
1531
+ /**
1532
+ * Block markers are at most a few characters (`'###### '`, `' 999. '`,
1533
+ * `'- [x] '`); when the caret is past this column a space cannot complete a
1534
+ * block marker, so the micromark scan is skipped entirely.
1535
+ */
1536
+ const MAX_BLOCK_MARKER_LENGTH = 24;
1537
+
1538
+ /** Removes the first `n` characters from the leading text nodes of `element`. */
1539
+ function $stripLeading(element, n) {
1540
+ let remaining = n;
1541
+ let child = element.getFirstChild();
1542
+ while (remaining > 0 && child) {
1543
+ if (!lexical.$isTextNode(child)) {
1544
+ break;
1545
+ }
1546
+ const text = child.getTextContent();
1547
+ if (text.length <= remaining) {
1548
+ const next = child.getNextSibling();
1549
+ remaining -= text.length;
1550
+ child.remove();
1551
+ child = next;
1552
+ } else {
1553
+ child.setTextContent(text.slice(remaining));
1554
+ remaining = 0;
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ /**
1560
+ * Replaces `paragraph` with the block construct described by `match`, keeping
1561
+ * the content that followed the marker.
1562
+ */
1563
+ function $applyBlock(paragraph, match) {
1564
+ // Capture the fence before the marker is stripped from the paragraph.
1565
+ const fence = match.kind === 'code' ? paragraph.getTextContent().match(/^[ \t]*(`{3,}|~{3,})/) : null;
1566
+ $stripLeading(paragraph, match.markerLength);
1567
+ const remaining = paragraph.getChildren();
1568
+ if (match.kind === 'code') {
1569
+ const code = codeCore.$createCodeNode(match.node.lang || undefined);
1570
+ // Keep the typed fence and info-string tail so export reproduces them.
1571
+ if (fence) {
1572
+ lexical.$setState(code, codeFenceState, fence[1]);
1573
+ }
1574
+ if (match.node.meta) {
1575
+ lexical.$setState(code, codeMetaState, match.node.meta);
1576
+ }
1577
+ $append(code, remaining);
1578
+ paragraph.replace(code);
1579
+ code.selectStart();
1580
+ return true;
1581
+ }
1582
+ let target;
1583
+ let selectInto;
1584
+ if (match.kind === 'heading') {
1585
+ const heading = $append(richText.$createHeadingNode(`h${match.node.depth}`), remaining);
1586
+ target = heading;
1587
+ selectInto = heading;
1588
+ } else if (match.kind === 'blockquote') {
1589
+ const quote = $append(richText.$createQuoteNode(), remaining);
1590
+ target = quote;
1591
+ selectInto = quote;
1592
+ } else {
1593
+ const listNode = match.node;
1594
+ const listType = $listTypeFromMdast(listNode);
1595
+ const start = listNode.ordered && listNode.start != null ? listNode.start : 1;
1596
+ const list$1 = list.$createListNode(listType, start);
1597
+ const firstItem = listNode.children[0];
1598
+ const checked = firstItem && firstItem.type === 'listItem' && typeof firstItem.checked === 'boolean' ? firstItem.checked : undefined;
1599
+ const item = $append(list.$createListItemNode(checked), remaining);
1600
+ $append(list$1, [item]);
1601
+ target = list$1;
1602
+ selectInto = item;
1603
+ }
1604
+ paragraph.replace(target);
1605
+ selectInto.selectStart();
1606
+ return true;
1607
+ }
1608
+
1609
+ /**
1610
+ * Cheap check that `text` (up to the caret) could plausibly contain a closed
1611
+ * inline construct ending in `closeChar`, before paying for a micromark parse.
1612
+ * A closing `)` needs a `](` link infix; any other delimiter needs an earlier
1613
+ * occurrence of itself to act as the opener.
1614
+ */
1615
+ function mayCloseInlineConstruct(text, closeChar) {
1616
+ return closeChar === ')' ? text.lastIndexOf('](') > 0 : text.lastIndexOf(closeChar, text.length - 2) !== -1;
1617
+ }
1618
+
1619
+ /**
1620
+ * Materializes the inline construct ending at `anchorOffset` inside
1621
+ * `anchorNode`, replacing the raw markdown span with formatted Lexical nodes.
1622
+ */
1623
+ function $applyInline(anchorNode, anchorOffset, scanner) {
1624
+ const upTo = anchorNode.getTextContent().slice(0, anchorOffset);
1625
+ const inlineNode = scanner.scanInline(upTo);
1626
+ if (!inlineNode || !inlineNode.position) {
1627
+ return false;
1628
+ }
1629
+ const start = inlineNode.position.start.offset ?? 0;
1630
+ const end = anchorOffset;
1631
+ let target;
1632
+ if (start <= 0) {
1633
+ [target] = anchorNode.splitText(end);
1634
+ } else {
1635
+ const parts = anchorNode.splitText(start, end);
1636
+ target = parts.length === 3 ? parts[1] : parts[parts.length - 1];
1637
+ }
1638
+ const lexicalNodes = scanner.importInline(inlineNode);
1639
+ if (lexicalNodes.length === 0) {
1640
+ return false;
1641
+ }
1642
+ let prev = lexicalNodes[0];
1643
+ target.replace(prev);
1644
+ for (let i = 1; i < lexicalNodes.length; i++) {
1645
+ prev.insertAfter(lexicalNodes[i]);
1646
+ prev = lexicalNodes[i];
1647
+ }
1648
+ if (lexical.$isTextNode(prev)) {
1649
+ const size = prev.getTextContentSize();
1650
+ prev.select(size, size);
1651
+ } else {
1652
+ prev.selectNext(0, 0);
1653
+ }
1654
+ return true;
1655
+ }
1656
+
1657
+ /**
1658
+ * Matches a GFM task-list checkbox marker (`[ ] `, `[x] `) typed at line
1659
+ * start. Exactly one character is required between the brackets, matching
1660
+ * micromark's gfm-task-list-item grammar — `[]` is not a checkbox.
1661
+ */
1662
+ const CHECKBOX_REGEX = /^\[([ xX])\]\s$/;
1663
+
1664
+ /**
1665
+ * Handles a checkbox marker typed at the start of a line. micromark only
1666
+ * recognizes a task-list item once it already has a list-item prefix, so by
1667
+ * the time the user types `[ ] ` the paragraph is usually already a bullet
1668
+ * list item (from the earlier `- ` shortcut). This promotes that item — or a
1669
+ * bare paragraph — to a Lexical check list.
1670
+ */
1671
+ function $tryCheckbox(anchorNode, parent, anchorOffset) {
1672
+ if (parent.getFirstChild() !== anchorNode) {
1673
+ return false;
1674
+ }
1675
+ const prefix = parent.getTextContent().slice(0, anchorOffset);
1676
+ const match = prefix.match(CHECKBOX_REGEX);
1677
+ if (!match) {
1678
+ return false;
1679
+ }
1680
+ const checked = match[1].toLowerCase() === 'x';
1681
+ if (list.$isListItemNode(parent)) {
1682
+ const list$1 = parent.getParent();
1683
+ if (!list.$isListNode(list$1) || list$1.getListType() === 'number') {
1684
+ return false;
1685
+ }
1686
+ $stripLeading(parent, match[0].length);
1687
+ list$1.setListType('check');
1688
+ parent.setChecked(checked);
1689
+ return true;
1690
+ }
1691
+ if ($isShortcutParagraph(parent, anchorNode)) {
1692
+ $stripLeading(parent, match[0].length);
1693
+ const remaining = parent.getChildren();
1694
+ const list$1 = list.$createListNode('check');
1695
+ const item = $append(list.$createListItemNode(checked), remaining);
1696
+ $append(list$1, [item]);
1697
+ parent.replace(list$1);
1698
+ item.selectStart();
1699
+ return true;
1700
+ }
1701
+ return false;
1702
+ }
1703
+ function $isShortcutParagraph(node, anchorNode) {
1704
+ return lexical.$isParagraphNode(node) && lexical.$isRootOrShadowRoot(node.getParent()) && node.getFirstChild() === anchorNode;
1705
+ }
1706
+
1707
+ /**
1708
+ * Registers streaming Markdown shortcuts on `editor` from the
1709
+ * {@link CompiledMdast} registry. As the user types, the current line/inline
1710
+ * buffer is fed back through micromark (the same parser as full-document
1711
+ * import) and recognized constructs are transformed in place:
1712
+ *
1713
+ * - Block markers (`# `, `> `, `- `, `1. `, `- [ ] `) convert the paragraph
1714
+ * into the matching Lexical block as soon as the trailing space is typed.
1715
+ * - A marker-only line (`` ```lang ``, `## `, `- `) converts on
1716
+ * <kbd>Enter</kbd>.
1717
+ * - Inline constructs (`*em*`, `**strong**`, `` `code` ``, `~~del~~`,
1718
+ * `[text](url)`, plus registered `inlineShortcutTypes`) convert when their
1719
+ * closing delimiter is typed.
1720
+ *
1721
+ * Wired up by {@link MdastShortcutsExtension}; this is an internal helper, not
1722
+ * part of the package's public API.
1723
+ */
1724
+ function registerMarkdownShortcuts(editor, compiled) {
1725
+ const scanner = new MarkdownStreamScanner(compiled);
1726
+ const inlineTriggers = scanner.inlineTriggers;
1727
+ // Composition end fires per IME commit (every CJK syllable, dead-key
1728
+ // resolve, ...). Only enter the transformer pass when the just-committed
1729
+ // character can plausibly close a trigger.
1730
+ const compositionEndTriggers = new Set([' ', ...inlineTriggers]);
1731
+ return lexical.mergeRegister(editor.registerUpdateListener(({
1732
+ tags,
1733
+ dirtyLeaves,
1734
+ editorState,
1735
+ prevEditorState
1736
+ }) => {
1737
+ // Ignore updates from collaboration and undo/redo (changes already
1738
+ // calculated), and anything that dirtied no leaves (pure selection
1739
+ // moves) before paying for any editor-state reads.
1740
+ if (dirtyLeaves.size === 0 || tags.has(lexical.COLLABORATION_TAG) || tags.has(lexical.HISTORIC_TAG)) {
1741
+ return;
1742
+ }
1743
+ // If the editor is still composing we must wait for the commit.
1744
+ if (editor.isComposing()) {
1745
+ return;
1746
+ }
1747
+ // A composition commit lands without moving the selection (and may
1748
+ // commit several characters at once), so it bypasses the typed-one-
1749
+ // character heuristics below.
1750
+ const isCompositionEnd = tags.has(lexical.COMPOSITION_END_TAG);
1751
+ const selection = editorState.read(lexical.$getSelection);
1752
+ const prevSelection = prevEditorState.read(lexical.$getSelection);
1753
+ if (!lexical.$isRangeSelection(selection) || !lexical.$isRangeSelection(prevSelection) || !selection.isCollapsed() || selection.is(prevSelection) && !isCompositionEnd) {
1754
+ return;
1755
+ }
1756
+ const anchorKey = selection.anchor.key;
1757
+ const anchorOffset = selection.anchor.offset;
1758
+ const anchorNode = editorState._nodeMap.get(anchorKey);
1759
+ if (!lexical.$isTextNode(anchorNode) || !dirtyLeaves.has(anchorKey)) {
1760
+ return;
1761
+ }
1762
+ // Only react to a single typed character: the caret must have
1763
+ // advanced exactly one position (or sit right after the first
1764
+ // character of a fresh node). This keeps paste, drag-drop, and
1765
+ // deletions — which can leave the caret after a delimiter — from
1766
+ // firing destructive transforms.
1767
+ if (!isCompositionEnd && anchorOffset !== 1 && !(prevSelection.anchor.key === anchorKey && anchorOffset === prevSelection.anchor.offset + 1)) {
1768
+ return;
1769
+ }
1770
+ const textContent = editorState.read(() => anchorNode.getTextContent());
1771
+ const typedChar = textContent[anchorOffset - 1];
1772
+ if (isCompositionEnd && !compositionEndTriggers.has(typedChar)) {
1773
+ return;
1774
+ }
1775
+ if (typedChar !== ' ' && !inlineTriggers.has(typedChar)) {
1776
+ return;
1777
+ }
1778
+ // Cheap prefilter: an inline construct needs an opener earlier in the
1779
+ // text; skip the micromark parse when there is none.
1780
+ if (typedChar !== ' ' && !mayCloseInlineConstruct(textContent.slice(0, anchorOffset), typedChar)) {
1781
+ return;
1782
+ }
1783
+ editor.update(() => {
1784
+ const node = lexical.$getNodeByKey(anchorKey);
1785
+ if (!lexical.$isTextNode(node) || node.hasFormat('code')) {
1786
+ // Per CommonMark, code spans take precedence over any other
1787
+ // inline construct; never transform inside one.
1788
+ return;
1789
+ }
1790
+ const parent = node.getParent();
1791
+ if (parent === null || codeCore.$isCodeNode(parent)) {
1792
+ return;
1793
+ }
1794
+ let transformed = false;
1795
+ if (typedChar === ' ') {
1796
+ if (scanner.supportsTaskListItems && $tryCheckbox(node, parent, anchorOffset)) {
1797
+ transformed = true;
1798
+ } else if (anchorOffset <= MAX_BLOCK_MARKER_LENGTH && $isShortcutParagraph(parent, node)) {
1799
+ // The marker must end exactly at the caret, so scanning the
1800
+ // prefix is sufficient (and cheaper than the whole line).
1801
+ const match = scanner.scanBlock(node.getTextContent().slice(0, anchorOffset));
1802
+ if (match && match.kind !== 'code' && match.markerLength === anchorOffset) {
1803
+ transformed = $applyBlock(parent, match);
1804
+ }
1805
+ }
1806
+ } else {
1807
+ transformed = $applyInline(node, anchorOffset, scanner);
1808
+ }
1809
+ if (transformed) {
1810
+ lexical.$addUpdateTag(lexical.HISTORY_PUSH_TAG);
1811
+ }
1812
+ });
1813
+ }), editor.registerCommand(lexical.KEY_ENTER_COMMAND, event => {
1814
+ if (event !== null && event.shiftKey) {
1815
+ return false;
1816
+ }
1817
+ const selection = lexical.$getSelection();
1818
+ if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed()) {
1819
+ return false;
1820
+ }
1821
+ const anchorNode = selection.anchor.getNode();
1822
+ if (!lexical.$isTextNode(anchorNode) || anchorNode.hasFormat('code')) {
1823
+ return false;
1824
+ }
1825
+ const parent = anchorNode.getParent();
1826
+ const anchorOffset = selection.anchor.offset;
1827
+ if (parent === null || codeCore.$isCodeNode(parent) || !$isShortcutParagraph(parent, anchorNode) || anchorOffset !== anchorNode.getTextContentSize()) {
1828
+ return false;
1829
+ }
1830
+ const match = scanner.scanBlock(parent.getTextContent());
1831
+ // Only convert when the whole line is the marker (`## `, `- `,
1832
+ // '```lang title=x'). A line with content after the marker was either
1833
+ // already converted at the trailing space, or deliberately reverted
1834
+ // with undo — Enter must not re-convert it.
1835
+ if (match && match.markerLength === anchorOffset && $applyBlock(parent, match)) {
1836
+ if (event !== null) {
1837
+ event.preventDefault();
1838
+ }
1839
+ return true;
1840
+ }
1841
+ return false;
1842
+ },
1843
+ // The lowest priority that still pre-empts the default rich-text Enter
1844
+ // handler: prepended to the editor-priority queue, so every listener at
1845
+ // LOW and above (and none of the defaults) runs first.
1846
+ lexical.COMMAND_PRIORITY_BEFORE_EDITOR));
1847
+ }
1848
+
1849
+ /**
1850
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1851
+ *
1852
+ * This source code is licensed under the MIT license found in the
1853
+ * LICENSE file in the root directory of this source tree.
1854
+ *
1855
+ */
1856
+
1857
+
1858
+ /**
1859
+ * Configuration for the core {@link MdastImportExtension} registry. Feature
1860
+ * extensions contribute to these arrays via `configExtension(MdastImportExtension,
1861
+ * …)`; you rarely need to set them by hand. The shape mirrors
1862
+ * `@lexical/html`'s `DOMImportExtension` config: raw contribution arrays that
1863
+ * `mergeConfig` concatenates and `build` compiles.
1864
+ * @experimental
1865
+ */
1866
+
1867
+ /**
1868
+ * The runtime API exposed by {@link MdastImportExtension}. Obtain it inside a
1869
+ * read/update with `$getExtensionOutput(MdastImportExtension)`, or use the
1870
+ * {@link $convertFromMarkdownString} shorthand. Serialization lives in
1871
+ * `MdastExportExtension` so import-only editors don't bundle the
1872
+ * serializer (`mdast-util-to-markdown`).
1873
+ * @experimental
1874
+ */
1875
+
1876
+ // The baseline rules that need no node packages: paragraphs and inline text
1877
+ // formatting (CommonMark handles these without any micromark extension).
1878
+ const CORE_IMPORT_RULES = [{
1879
+ $import: $importParagraph,
1880
+ type: 'paragraph'
1881
+ }, {
1882
+ $import: importText,
1883
+ type: 'text'
1884
+ }, {
1885
+ $import: importHtml,
1886
+ type: 'html'
1887
+ }, {
1888
+ $import: importInlineCode,
1889
+ type: 'inlineCode'
1890
+ }, {
1891
+ $import: $importEmphasis,
1892
+ type: 'emphasis'
1893
+ }, {
1894
+ $import: $importStrong,
1895
+ type: 'strong'
1896
+ }, {
1897
+ $import: $importBreak,
1898
+ type: 'break'
1899
+ }];
1900
+ const CORE_EXPORT_RULES = [{
1901
+ $export: exportParagraph,
1902
+ type: 'paragraph'
1903
+ }, {
1904
+ $export: exportText,
1905
+ type: 'text'
1906
+ }, {
1907
+ $export: $exportLineBreak,
1908
+ type: 'linebreak'
1909
+ }, {
1910
+ $export: exportTab,
1911
+ type: 'tab'
1912
+ }];
1913
+
1914
+ /**
1915
+ * The core Markdown registry for `@lexical/mdast`, modeled on
1916
+ * `@lexical/html`'s `DOMImportExtension`. It assembles the import/export rules
1917
+ * and micromark/mdast extensions contributed by feature extensions into a
1918
+ * compiled registry, and exposes Markdown import through its
1919
+ * {@link MdastImportExtensionOutput}. Markdown export is provided separately by
1920
+ * `MdastExportExtension`, so editors that never serialize back to Markdown
1921
+ * don't bundle the serializer.
1922
+ *
1923
+ * You normally do not depend on this directly — depend on a feature extension
1924
+ * (e.g. {@link MdastCommonMarkExtension}) which contributes its rules here and
1925
+ * ships the nodes those rules need.
1926
+ *
1927
+ * @example
1928
+ * ```ts
1929
+ * import {$convertFromMarkdownString, MdastCommonMarkExtension}
1930
+ * from '@lexical/mdast';
1931
+ * import {buildEditorFromExtensions} from '@lexical/extension';
1932
+ * import {defineExtension} from 'lexical';
1933
+ *
1934
+ * const editor = buildEditorFromExtensions(
1935
+ * defineExtension({dependencies: [MdastCommonMarkExtension], name: '[root]'}),
1936
+ * );
1937
+ * editor.update(() => $convertFromMarkdownString('# Hi'));
1938
+ * ```
1939
+ * @experimental
1940
+ */
1941
+ const MdastImportExtension = /* @__PURE__ */lexical.defineExtension({
1942
+ build(editor, config) {
1943
+ const registry = compileMdast(config);
1944
+ const {
1945
+ $generateNodesFromMarkdown,
1946
+ $generateNodesFromMdast: $generateNodesFromTree,
1947
+ $importMarkdown,
1948
+ $importMdast
1949
+ } = createMdastImport(registry);
1950
+ return {
1951
+ $convertFromMarkdownString: $importMarkdown,
1952
+ $convertFromMdast: $importMdast,
1953
+ $generateNodesFromMarkdownString: $generateNodesFromMarkdown,
1954
+ $generateNodesFromMdast: $generateNodesFromTree,
1955
+ registry
1956
+ };
1957
+ },
1958
+ config: /* @__PURE__ */lexical.safeCast({
1959
+ exportRules: CORE_EXPORT_RULES,
1960
+ importRules: CORE_IMPORT_RULES,
1961
+ // Core CommonMark inline formatting; feature extensions contribute
1962
+ // their own types/triggers (links add 'link'/')', strikethrough adds
1963
+ // 'delete'/'~').
1964
+ inlineShortcutTriggers: ['*', '_', '`'],
1965
+ inlineShortcutTypes: ['emphasis', 'inlineCode', 'strong'],
1966
+ mdastExtensions: [],
1967
+ micromarkExtensions: [],
1968
+ toMarkdownExtensions: []
1969
+ }),
1970
+ mergeConfig(config, partial) {
1971
+ // Prepend contributed rules so extensions merged later (closer to the
1972
+ // editor root) take priority, matching DOMImportExtension's convention.
1973
+ // Every key is set explicitly so an explicitly-undefined key in `partial`
1974
+ // (allowed by Partial<MdastConfig>) can never clobber the merged arrays.
1975
+ function mergeArray(contributed, existing) {
1976
+ return contributed ? [...contributed, ...existing] : existing;
1977
+ }
1978
+ return lexical.shallowMergeConfig(config, {
1979
+ exportRules: mergeArray(partial.exportRules, config.exportRules),
1980
+ importRules: mergeArray(partial.importRules, config.importRules),
1981
+ inlineShortcutTriggers: mergeArray(partial.inlineShortcutTriggers, config.inlineShortcutTriggers),
1982
+ inlineShortcutTypes: mergeArray(partial.inlineShortcutTypes, config.inlineShortcutTypes),
1983
+ mdastExtensions: mergeArray(partial.mdastExtensions, config.mdastExtensions),
1984
+ micromarkExtensions: mergeArray(partial.micromarkExtensions, config.micromarkExtensions),
1985
+ toMarkdownExtensions: mergeArray(partial.toMarkdownExtensions, config.toMarkdownExtensions)
1986
+ });
1987
+ },
1988
+ name: '@lexical/mdast/Import'
1989
+ });
1990
+
1991
+ /**
1992
+ * ATX (`# …`) and setext headings, shipping {@link HeadingNode}.
1993
+ * @experimental
1994
+ */
1995
+ const MdastHeadingExtension = /* @__PURE__ */lexical.defineExtension({
1996
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
1997
+ exportRules: [{
1998
+ $export: $exportHeading,
1999
+ type: 'heading'
2000
+ }],
2001
+ importRules: [{
2002
+ $import: $importHeading,
2003
+ type: 'heading'
2004
+ }]
2005
+ })],
2006
+ name: '@lexical/mdast/Heading',
2007
+ nodes: [richText.HeadingNode]
2008
+ });
2009
+
2010
+ /**
2011
+ * Block quotes (`> …`), shipping {@link QuoteNode}. For blockquotes that hold
2012
+ * block-level children (nested lists, code, quotes) with full fidelity, add
2013
+ * {@link MdastShadowRootQuoteExtension}.
2014
+ * @experimental
2015
+ */
2016
+ const MdastBlockquoteExtension = /* @__PURE__ */lexical.defineExtension({
2017
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2018
+ exportRules: [{
2019
+ $export: exportQuote,
2020
+ type: 'quote'
2021
+ }],
2022
+ importRules: [{
2023
+ $import: $importBlockquote,
2024
+ type: 'blockquote'
2025
+ }]
2026
+ })],
2027
+ name: '@lexical/mdast/Blockquote',
2028
+ nodes: [richText.QuoteNode]
2029
+ });
2030
+
2031
+ /**
2032
+ * Convenience bundle of {@link MdastHeadingExtension} and
2033
+ * {@link MdastBlockquoteExtension} — the constructs backed by
2034
+ * `@lexical/rich-text` nodes.
2035
+ * @experimental
2036
+ */
2037
+ const MdastRichTextExtension = /* @__PURE__ */lexical.defineExtension({
2038
+ dependencies: [MdastHeadingExtension, MdastBlockquoteExtension],
2039
+ name: '@lexical/mdast/RichText'
2040
+ });
2041
+
2042
+ /**
2043
+ * Ordered and unordered lists, shipping {@link ListNode} and
2044
+ * {@link ListItemNode}. For GFM task lists (`- [x] …`) add
2045
+ * {@link MdastTaskListExtension}.
2046
+ * @experimental
2047
+ */
2048
+ const MdastListExtension = /* @__PURE__ */lexical.defineExtension({
2049
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2050
+ exportRules: [{
2051
+ $export: $exportList,
2052
+ type: 'list'
2053
+ }],
2054
+ importRules: [{
2055
+ $import: $importList,
2056
+ type: 'list'
2057
+ }, {
2058
+ $import: $importListItem,
2059
+ type: 'listItem'
2060
+ }]
2061
+ })],
2062
+ name: '@lexical/mdast/List',
2063
+ nodes: [list.ListNode, list.ListItemNode]
2064
+ });
2065
+
2066
+ /**
2067
+ * Opt-in: GFM task lists (`- [x] done`), layered on
2068
+ * {@link MdastListExtension}. Contributes the `gfmTaskListItem` grammar; the
2069
+ * list import/export handlers already understand `checked`, and the typing
2070
+ * shortcut (`[ ] ` / `[x] ` in a list item) is enabled by the grammar's
2071
+ * presence in the registry.
2072
+ * @experimental
2073
+ */
2074
+ const MdastTaskListExtension = /* @__PURE__ */lexical.defineExtension({
2075
+ dependencies: [MdastListExtension, /* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2076
+ mdastExtensions: [/* @__PURE__ */mdastUtilGfmTaskListItem.gfmTaskListItemFromMarkdown()],
2077
+ micromarkExtensions: [/* @__PURE__ */micromarkExtensionGfmTaskListItem.gfmTaskListItem()],
2078
+ toMarkdownExtensions: [/* @__PURE__ */mdastUtilGfmTaskListItem.gfmTaskListItemToMarkdown()]
2079
+ })],
2080
+ name: '@lexical/mdast/TaskList'
2081
+ });
2082
+
2083
+ /**
2084
+ * Fenced and indented code blocks, shipping {@link CodeNode}.
2085
+ * @experimental
2086
+ */
2087
+ const MdastCodeExtension = /* @__PURE__ */lexical.defineExtension({
2088
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2089
+ exportRules: [{
2090
+ $export: $exportCode,
2091
+ type: 'code'
2092
+ }],
2093
+ importRules: [{
2094
+ $import: $importCode,
2095
+ type: 'code'
2096
+ }]
2097
+ })],
2098
+ name: '@lexical/mdast/Code',
2099
+ nodes: [codeCore.CodeNode]
2100
+ });
2101
+
2102
+ /**
2103
+ * Inline links, CommonMark autolinks (`<https://…>`), and CommonMark
2104
+ * reference links (`[text][id]` resolved against `[id]: url` definitions),
2105
+ * shipping {@link LinkNode}. Reference links are resolved to their target on
2106
+ * import and serialize back as inline links. For GFM *literal* autolinks
2107
+ * (bare `https://…` in prose) add {@link MdastAutolinkLiteralExtension}.
2108
+ * @experimental
2109
+ */
2110
+ const MdastLinkExtension = /* @__PURE__ */lexical.defineExtension({
2111
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2112
+ exportRules: [{
2113
+ $export: $exportLink,
2114
+ type: 'link'
2115
+ }],
2116
+ importRules: [{
2117
+ $import: $importLink,
2118
+ type: 'link'
2119
+ }, {
2120
+ $import: $importLinkReference,
2121
+ type: 'linkReference'
2122
+ }, {
2123
+ $import: importDefinition,
2124
+ type: 'definition'
2125
+ }],
2126
+ inlineShortcutTriggers: [')'],
2127
+ inlineShortcutTypes: ['link']
2128
+ })],
2129
+ name: '@lexical/mdast/Link',
2130
+ nodes: [link.LinkNode]
2131
+ });
2132
+
2133
+ /**
2134
+ * Opt-in: GFM literal autolinks — bare `https://…` / `www.…` URLs and email
2135
+ * addresses in prose become links, the way GitHub renders them. This is a GFM
2136
+ * extension rather than CommonMark, so it is not part of
2137
+ * {@link MdastCommonMarkExtension}; add it alongside to opt in:
2138
+ * ```ts
2139
+ * dependencies: [MdastCommonMarkExtension, MdastAutolinkLiteralExtension]
2140
+ * ```
2141
+ * @experimental
2142
+ */
2143
+ const MdastAutolinkLiteralExtension = /* @__PURE__ */lexical.defineExtension({
2144
+ dependencies: [MdastLinkExtension, /* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2145
+ mdastExtensions: [/* @__PURE__ */mdastUtilGfmAutolinkLiteral.gfmAutolinkLiteralFromMarkdown()],
2146
+ micromarkExtensions: [/* @__PURE__ */micromarkExtensionGfmAutolinkLiteral.gfmAutolinkLiteral()],
2147
+ toMarkdownExtensions: [/* @__PURE__ */mdastUtilGfmAutolinkLiteral.gfmAutolinkLiteralToMarkdown()]
2148
+ })],
2149
+ name: '@lexical/mdast/AutolinkLiteral'
2150
+ });
2151
+
2152
+ /**
2153
+ * Opt-in: import Markdown blockquotes as *shadow root* {@link QuoteNode}s
2154
+ * (`$createQuoteNode({shadowRoot: true})`), which hold block-level children
2155
+ * like a table cell. Structured blockquotes — multiple paragraphs, nested
2156
+ * lists, code blocks, nested quotes — then round-trip with full fidelity
2157
+ * instead of being reassembled from inline content.
2158
+ *
2159
+ * Not part of {@link MdastCommonMarkExtension}; add it alongside to opt in:
2160
+ * ```ts
2161
+ * dependencies: [MdastCommonMarkExtension, MdastShadowRootQuoteExtension]
2162
+ * ```
2163
+ * The quote *export* handler supports both forms per node, so legacy quotes
2164
+ * (e.g. created by the `> ` shortcut) and shadow root quotes can coexist.
2165
+ * @experimental
2166
+ */
2167
+ const MdastShadowRootQuoteExtension = /* @__PURE__ */lexical.defineExtension({
2168
+ dependencies: [MdastBlockquoteExtension,
2169
+ // Declared after (and depending on) MdastBlockquoteExtension so this
2170
+ // blockquote rule merges later and takes priority over the default.
2171
+ /* @__PURE__ */
2172
+ lexical.configExtension(MdastImportExtension, {
2173
+ importRules: [{
2174
+ $import: $importShadowRootBlockquote,
2175
+ type: 'blockquote'
2176
+ }]
2177
+ })],
2178
+ name: '@lexical/mdast/ShadowRootQuote'
2179
+ });
2180
+ const $importThematicBreak = (node, ctx) => {
2181
+ const hr = extension.$createHorizontalRuleNode();
2182
+ // Preserve the marker character (`---` vs `***` vs `___`).
2183
+ if (ctx.source && node.position && node.position.start.offset != null) {
2184
+ const marker = ctx.source.slice(node.position.start.offset, node.position.start.offset + 4).trimStart()[0];
2185
+ if (marker === '-' || marker === '*' || marker === '_') {
2186
+ lexical.$setState(hr, hrMarkerState, marker);
2187
+ }
2188
+ }
2189
+ return hr;
2190
+ };
2191
+ const $exportThematicBreak = node => {
2192
+ if (!extension.$isHorizontalRuleNode(node)) {
2193
+ return null;
2194
+ }
2195
+ const rule = {
2196
+ type: 'thematicBreak'
2197
+ };
2198
+ const marker = lexical.$getState(node, hrMarkerState);
2199
+ if (marker) {
2200
+ rule.data = {
2201
+ mdastRule: marker
2202
+ };
2203
+ }
2204
+ return rule;
2205
+ };
2206
+
2207
+ /**
2208
+ * Thematic breaks (`---`, `***`, `___`), mapped to
2209
+ * {@link HorizontalRuleExtension}'s `HorizontalRuleNode`. The original marker
2210
+ * character is preserved on round-trip.
2211
+ * @experimental
2212
+ */
2213
+ const MdastHorizontalRuleExtension = /* @__PURE__ */lexical.defineExtension({
2214
+ dependencies: [extension.HorizontalRuleExtension, /* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2215
+ exportRules: [{
2216
+ $export: $exportThematicBreak,
2217
+ type: 'horizontalrule'
2218
+ }],
2219
+ importRules: [{
2220
+ $import: $importThematicBreak,
2221
+ type: 'thematicBreak'
2222
+ }]
2223
+ })],
2224
+ name: '@lexical/mdast/HorizontalRule'
2225
+ });
2226
+
2227
+ /**
2228
+ * GFM `~~strikethrough~~`, mapped to the Lexical `strikethrough` text format.
2229
+ * Needs no extra nodes (the core text handlers carry the format bit).
2230
+ * @experimental
2231
+ */
2232
+ const MdastStrikethroughExtension = /* @__PURE__ */lexical.defineExtension({
2233
+ dependencies: [/* @__PURE__ */lexical.configExtension(MdastImportExtension, {
2234
+ importRules: [{
2235
+ $import: importDelete,
2236
+ type: 'delete'
2237
+ }],
2238
+ inlineShortcutTriggers: ['~'],
2239
+ inlineShortcutTypes: ['delete'],
2240
+ mdastExtensions: [/* @__PURE__ */mdastUtilGfmStrikethrough.gfmStrikethroughFromMarkdown()],
2241
+ micromarkExtensions: [/* @__PURE__ */micromarkExtensionGfmStrikethrough.gfmStrikethrough()],
2242
+ toMarkdownExtensions: [/* @__PURE__ */mdastUtilGfmStrikethrough.gfmStrikethroughToMarkdown()]
2243
+ })],
2244
+ name: '@lexical/mdast/Strikethrough'
2245
+ });
2246
+
2247
+ /**
2248
+ * Convenience bundle of every CommonMark construct: headings, block quotes,
2249
+ * lists, code blocks, links, and thematic breaks. GFM features
2250
+ * (strikethrough, task lists, literal autolinks, tables) are bundled
2251
+ * separately as `MdastGfmExtension`, and `MdastExportExtension` (or the
2252
+ * `MdastExtension` bundle) adds serialization back to Markdown.
2253
+ * @experimental
2254
+ */
2255
+ const MdastCommonMarkExtension = /* @__PURE__ */lexical.defineExtension({
2256
+ dependencies: [MdastRichTextExtension, MdastListExtension, MdastCodeExtension, MdastLinkExtension, MdastHorizontalRuleExtension],
2257
+ name: '@lexical/mdast/CommonMark'
2258
+ });
2259
+ /**
2260
+ * Streaming Markdown shortcuts (block markers convert on space, fenced code on
2261
+ * Enter, inline constructs on their closing delimiter). Each keystroke is fed
2262
+ * back through micromark, so shortcut recognition uses the same grammar and
2263
+ * the same enabled extensions as import: shortcuts exist for exactly the
2264
+ * feature extensions in the editor and no others. Combine with
2265
+ * {@link MdastCommonMarkExtension} (and `MdastGfmExtension`) — this extension
2266
+ * only wires up the behavior, it does not pull in any grammar of its own.
2267
+ * @experimental
2268
+ */
2269
+ const MdastShortcutsExtension = /* @__PURE__ */lexical.defineExtension({
2270
+ build: (editor, config) => extension.namedSignals(config),
2271
+ config: /* @__PURE__ */lexical.safeCast({
2272
+ disabled: false
2273
+ }),
2274
+ dependencies: [MdastImportExtension],
2275
+ name: '@lexical/mdast/Shortcuts',
2276
+ register: (editor, config, state) => {
2277
+ const {
2278
+ disabled
2279
+ } = state.getOutput();
2280
+ return extension.effect(() => {
2281
+ if (disabled.value) {
2282
+ return undefined;
2283
+ }
2284
+ const {
2285
+ registry
2286
+ } = extension.getExtensionDependencyFromEditor(editor, MdastImportExtension).output;
2287
+ return registerMarkdownShortcuts(editor, registry);
2288
+ });
2289
+ }
2290
+ });
2291
+
2292
+ /**
2293
+ * Shorthand for `$getExtensionOutput(MdastImportExtension).$convertFromMarkdownString`.
2294
+ * Must be called inside an `editor.update()`. Throws if the editor was not
2295
+ * built with {@link MdastImportExtension} (or an extension that depends on it).
2296
+ * @experimental
2297
+ */
2298
+ function $convertFromMarkdownString(markdown, node) {
2299
+ extension.$getExtensionOutput(MdastImportExtension).$convertFromMarkdownString(markdown, node);
2300
+ }
2301
+
2302
+ /**
2303
+ * Shorthand for `$getExtensionOutput(MdastImportExtension).$convertFromMdast`.
2304
+ * Must be called inside an `editor.update()`. Throws if the editor was not
2305
+ * built with {@link MdastImportExtension} (or an extension that depends on
2306
+ * it).
2307
+ * @experimental
2308
+ */
2309
+ function $convertFromMdast(tree, node) {
2310
+ extension.$getExtensionOutput(MdastImportExtension).$convertFromMdast(tree, node);
2311
+ }
2312
+
2313
+ /**
2314
+ * Shorthand for
2315
+ * `$getExtensionOutput(MdastImportExtension).$generateNodesFromMarkdownString`.
2316
+ * Parses `markdown` and returns the resulting block-level nodes as a
2317
+ * detached array, without modifying the document or the selection. Must be
2318
+ * called inside an `editor.update()`. Throws if the editor was not built
2319
+ * with {@link MdastImportExtension} (or an extension that depends on it).
2320
+ * @experimental
2321
+ */
2322
+ function $generateNodesFromMarkdownString(markdown) {
2323
+ return extension.$getExtensionOutput(MdastImportExtension).$generateNodesFromMarkdownString(markdown);
2324
+ }
2325
+
2326
+ /**
2327
+ * Shorthand for
2328
+ * `$getExtensionOutput(MdastImportExtension).$generateNodesFromMdast`.
2329
+ * Walks an already-parsed mdast `Root` tree and returns the resulting
2330
+ * block-level nodes as a detached array, without modifying the document or
2331
+ * the selection. Must be called inside an `editor.update()`. Throws if the
2332
+ * editor was not built with {@link MdastImportExtension} (or an extension
2333
+ * that depends on it).
2334
+ * @experimental
2335
+ */
2336
+ function $generateNodesFromMdast(tree) {
2337
+ return extension.$getExtensionOutput(MdastImportExtension).$generateNodesFromMdast(tree);
2338
+ }
2339
+
2340
+ /**
2341
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
2342
+ *
2343
+ * This source code is licensed under the MIT license found in the
2344
+ * LICENSE file in the root directory of this source tree.
2345
+ *
2346
+ */
2347
+
2348
+
2349
+ /**
2350
+ * The runtime API exposed by {@link MdastExportExtension}. Obtain it inside a
2351
+ * read/update with `$getExtensionOutput(MdastExportExtension)`, or use the
2352
+ * {@link $convertToMarkdownString} shorthand.
2353
+ * @experimental
2354
+ */
2355
+
2356
+ /**
2357
+ * Markdown serialization for `@lexical/mdast`. Import
2358
+ * (`MdastImportExtension` and the feature extensions that contribute to it) and
2359
+ * export are separate extensions so that editors which only *parse* Markdown
2360
+ * — never serialize back — don't bundle `mdast-util-to-markdown`.
2361
+ *
2362
+ * The export rules themselves are contributed by the same feature extensions
2363
+ * that contribute import rules; this extension compiles the shared registry
2364
+ * into a serializer:
2365
+ * ```ts
2366
+ * dependencies: [MdastCommonMarkExtension, MdastExportExtension]
2367
+ * ```
2368
+ * @experimental
2369
+ */
2370
+ const MdastExportExtension = /* @__PURE__ */lexical.defineExtension({
2371
+ build(editor, config, state) {
2372
+ const {
2373
+ registry
2374
+ } = state.getDependency(MdastImportExtension).output;
2375
+ const {
2376
+ $exportSelectionToMarkdown,
2377
+ $exportToMdast,
2378
+ $exportToMarkdown
2379
+ } = createMdastExport(registry);
2380
+ return {
2381
+ $convertSelectionToMarkdownString: $exportSelectionToMarkdown,
2382
+ $convertToMarkdownString: $exportToMarkdown,
2383
+ $convertToMdast: $exportToMdast
2384
+ };
2385
+ },
2386
+ dependencies: [MdastImportExtension],
2387
+ name: '@lexical/mdast/Export'
2388
+ });
2389
+
2390
+ /**
2391
+ * Shorthand for
2392
+ * `$getExtensionOutput(MdastExportExtension).$convertToMarkdownString`.
2393
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
2394
+ * the editor was not built with {@link MdastExportExtension}.
2395
+ * @experimental
2396
+ */
2397
+ function $convertToMarkdownString(node) {
2398
+ return extension.$getExtensionOutput(MdastExportExtension).$convertToMarkdownString(node);
2399
+ }
2400
+
2401
+ /**
2402
+ * Shorthand for `$getExtensionOutput(MdastExportExtension).$convertToMdast`.
2403
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
2404
+ * the editor was not built with {@link MdastExportExtension}.
2405
+ * @experimental
2406
+ */
2407
+ function $convertToMdast(node) {
2408
+ return extension.$getExtensionOutput(MdastExportExtension).$convertToMdast(node);
2409
+ }
2410
+
2411
+ /**
2412
+ * Shorthand for
2413
+ * `$getExtensionOutput(MdastExportExtension).$convertSelectionToMarkdownString`.
2414
+ * Serializes only the selected content (defaulting to the current selection)
2415
+ * to a Markdown string; returns `''` for a null or collapsed selection.
2416
+ * Must be called inside an `editor.read()` or `editor.update()`. Throws if
2417
+ * the editor was not built with {@link MdastExportExtension}.
2418
+ * @experimental
2419
+ */
2420
+ function $convertSelectionToMarkdownString(selection) {
2421
+ return extension.$getExtensionOutput(MdastExportExtension).$convertSelectionToMarkdownString(selection);
2422
+ }
2423
+
2424
+ /**
2425
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
2426
+ *
2427
+ * This source code is licensed under the MIT license found in the
2428
+ * LICENSE file in the root directory of this source tree.
2429
+ *
2430
+ */
2431
+
2432
+
2433
+ /**
2434
+ * Convenience bundle of {@link MdastImportExtension} and
2435
+ * {@link MdastExportExtension}: Markdown parsing *and* serialization.
2436
+ *
2437
+ * Depend on this when you want both directions without thinking about it:
2438
+ * ```ts
2439
+ * dependencies: [MdastCommonMarkExtension, MdastExtension]
2440
+ * ```
2441
+ * Editors that never serialize back to Markdown can skip it (feature
2442
+ * extensions already pull in {@link MdastImportExtension}) and avoid
2443
+ * bundling the serializer (`mdast-util-to-markdown`).
2444
+ * @experimental
2445
+ */
2446
+ const MdastExtension = /* @__PURE__ */lexical.defineExtension({
2447
+ dependencies: [MdastImportExtension, MdastExportExtension],
2448
+ name: '@lexical/mdast/Mdast'
2449
+ });
2450
+
2451
+ /**
2452
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
2453
+ *
2454
+ * This source code is licensed under the MIT license found in the
2455
+ * LICENSE file in the root directory of this source tree.
2456
+ *
2457
+ */
2458
+
2459
+
2460
+ /** The per-column alignment (`| :-: |`) a table's delimiter row declared. */
2461
+ const tableAlignState = /* @__PURE__ */lexical.createState('mdastTableAlign', {
2462
+ parse: v => Array.isArray(v) ? v.map(a => a === 'center' || a === 'left' || a === 'right' ? a : null) : [],
2463
+ resetOnCopyNode: true
2464
+ });
2465
+ const $importTable = (node, ctx) => {
2466
+ const table$1 = table.$createTableNode();
2467
+ if (node.align && node.align.some(a => a != null)) {
2468
+ lexical.$setState(table$1, tableAlignState, node.align);
2469
+ }
2470
+ node.children.forEach((row, rowIndex) => {
2471
+ const rowNode = table.$createTableRowNode();
2472
+ for (const cell of row.children) {
2473
+ const cellNode = table.$createTableCellNode(rowIndex === 0 ? table.TableCellHeaderStates.ROW : table.TableCellHeaderStates.NO_STATUS);
2474
+ const paragraph = lexical.$createParagraphNode();
2475
+ $append(paragraph, ctx.importChildren(cell));
2476
+ $append(cellNode, [paragraph]);
2477
+ $append(rowNode, [cellNode]);
2478
+ }
2479
+ $append(table$1, [rowNode]);
2480
+ });
2481
+ return table$1;
2482
+ };
2483
+ const $exportTable = (node, ctx) => {
2484
+ if (!table.$isTableNode(node)) {
2485
+ return null;
2486
+ }
2487
+ const rows = [];
2488
+ for (const row of node.getChildren()) {
2489
+ // Structural iteration bypasses the walk's selection filter, so rows a
2490
+ // selection export does not reach are skipped here. Cells stay: dropping
2491
+ // one would shift the remaining cells into other columns.
2492
+ if (!table.$isTableRowNode(row) || !ctx.isIncluded(row)) {
2493
+ continue;
2494
+ }
2495
+ const cells = [];
2496
+ for (const cell of row.getChildren()) {
2497
+ if (!table.$isTableCellNode(cell)) {
2498
+ continue;
2499
+ }
2500
+ const children = [];
2501
+ for (const child of cell.getChildren()) {
2502
+ if (lexical.$isElementNode(child)) {
2503
+ // GFM cells hold a single line of phrasing content; multiple block
2504
+ // children (paragraphs from Enter inside the cell) are joined with
2505
+ // hard breaks, which gfm-table serializes as spaces inside the cell.
2506
+ if (children.length > 0) {
2507
+ children.push({
2508
+ type: 'break'
2509
+ });
2510
+ }
2511
+ children.push(...ctx.exportInline(child));
2512
+ }
2513
+ }
2514
+ cells.push({
2515
+ children,
2516
+ type: 'tableCell'
2517
+ });
2518
+ }
2519
+ rows.push({
2520
+ children: cells,
2521
+ type: 'tableRow'
2522
+ });
2523
+ }
2524
+ return {
2525
+ align: lexical.$getState(node, tableAlignState),
2526
+ children: rows,
2527
+ type: 'table'
2528
+ };
2529
+ };
2530
+
2531
+ /**
2532
+ * GFM tables, mapped to `@lexical/table` nodes. Opt-in (not part of
2533
+ * {@link MdastCommonMarkExtension}) because it pulls in the `@lexical/table`
2534
+ * nodes it ships. The first table row is treated as the header row in both
2535
+ * directions.
2536
+ *
2537
+ * @example
2538
+ * ```ts
2539
+ * import {MdastShortcutsExtension, MdastTableExtension} from '@lexical/mdast';
2540
+ * import {buildEditorFromExtensions} from '@lexical/extension';
2541
+ * import {defineExtension} from 'lexical';
2542
+ *
2543
+ * const editor = buildEditorFromExtensions(
2544
+ * defineExtension({
2545
+ * dependencies: [MdastShortcutsExtension, MdastTableExtension],
2546
+ * name: '[root]',
2547
+ * }),
2548
+ * );
2549
+ * ```
2550
+ * @experimental
2551
+ */
2552
+ const MdastTableExtension = /* @__PURE__ */lexical.defineExtension({
2553
+ dependencies: [/* @__PURE__ */extension.configExtension(MdastImportExtension, {
2554
+ exportRules: [{
2555
+ $export: $exportTable,
2556
+ type: 'table'
2557
+ }],
2558
+ importRules: [{
2559
+ $import: $importTable,
2560
+ type: 'table'
2561
+ }],
2562
+ mdastExtensions: [/* @__PURE__ */mdastUtilGfmTable.gfmTableFromMarkdown()],
2563
+ micromarkExtensions: [/* @__PURE__ */micromarkExtensionGfmTable.gfmTable()],
2564
+ toMarkdownExtensions: [/* @__PURE__ */mdastUtilGfmTable.gfmTableToMarkdown()]
2565
+ })],
2566
+ name: '@lexical/mdast/Table',
2567
+ nodes: [table.TableNode, table.TableRowNode, table.TableCellNode]
2568
+ });
2569
+
2570
+ /**
2571
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
2572
+ *
2573
+ * This source code is licensed under the MIT license found in the
2574
+ * LICENSE file in the root directory of this source tree.
2575
+ *
2576
+ */
2577
+
2578
+
2579
+ /**
2580
+ * Convenience bundle of every GFM extension — strikethrough, task lists,
2581
+ * literal autolinks, and tables — mirroring the scope of
2582
+ * `micromark-extension-gfm`. Combine with `MdastCommonMarkExtension` for
2583
+ * GitHub-flavored Markdown:
2584
+ * ```ts
2585
+ * dependencies: [MdastCommonMarkExtension, MdastGfmExtension]
2586
+ * ```
2587
+ * Each member is also usable individually when you only want some of GFM
2588
+ * (e.g. task lists without tables).
2589
+ * @experimental
2590
+ */
2591
+ const MdastGfmExtension = /* @__PURE__ */lexical.defineExtension({
2592
+ dependencies: [MdastStrikethroughExtension, MdastTaskListExtension, MdastAutolinkLiteralExtension, MdastTableExtension],
2593
+ name: '@lexical/mdast/Gfm'
2594
+ });
2595
+
2596
+ exports.$convertFromMarkdownString = $convertFromMarkdownString;
2597
+ exports.$convertFromMdast = $convertFromMdast;
2598
+ exports.$convertSelectionToMarkdownString = $convertSelectionToMarkdownString;
2599
+ exports.$convertToMarkdownString = $convertToMarkdownString;
2600
+ exports.$convertToMdast = $convertToMdast;
2601
+ exports.$generateNodesFromMarkdownString = $generateNodesFromMarkdownString;
2602
+ exports.$generateNodesFromMdast = $generateNodesFromMdast;
2603
+ exports.MdastAutolinkLiteralExtension = MdastAutolinkLiteralExtension;
2604
+ exports.MdastBlockquoteExtension = MdastBlockquoteExtension;
2605
+ exports.MdastCodeExtension = MdastCodeExtension;
2606
+ exports.MdastCommonMarkExtension = MdastCommonMarkExtension;
2607
+ exports.MdastExportExtension = MdastExportExtension;
2608
+ exports.MdastExtension = MdastExtension;
2609
+ exports.MdastGfmExtension = MdastGfmExtension;
2610
+ exports.MdastHeadingExtension = MdastHeadingExtension;
2611
+ exports.MdastHorizontalRuleExtension = MdastHorizontalRuleExtension;
2612
+ exports.MdastImportExtension = MdastImportExtension;
2613
+ exports.MdastLinkExtension = MdastLinkExtension;
2614
+ exports.MdastListExtension = MdastListExtension;
2615
+ exports.MdastRichTextExtension = MdastRichTextExtension;
2616
+ exports.MdastShadowRootQuoteExtension = MdastShadowRootQuoteExtension;
2617
+ exports.MdastShortcutsExtension = MdastShortcutsExtension;
2618
+ exports.MdastStrikethroughExtension = MdastStrikethroughExtension;
2619
+ exports.MdastTableExtension = MdastTableExtension;
2620
+ exports.MdastTaskListExtension = MdastTaskListExtension;