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