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