@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
package/src/handlers.ts
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
MdastExportContext,
|
|
11
|
+
MdastExportHandler,
|
|
12
|
+
MdastImportContext,
|
|
13
|
+
MdastImportHandler,
|
|
14
|
+
MdastParent,
|
|
15
|
+
} from './types';
|
|
16
|
+
import type {ListItemNode, ListNode, ListType} from '@lexical/list';
|
|
17
|
+
import type {HeadingTagType} from '@lexical/rich-text';
|
|
18
|
+
import type {ElementNode, LexicalNode, LineBreakNode} from 'lexical';
|
|
19
|
+
import type {
|
|
20
|
+
Blockquote,
|
|
21
|
+
Break,
|
|
22
|
+
Code,
|
|
23
|
+
Delete,
|
|
24
|
+
Emphasis,
|
|
25
|
+
Heading,
|
|
26
|
+
Html,
|
|
27
|
+
InlineCode,
|
|
28
|
+
Link,
|
|
29
|
+
LinkReference,
|
|
30
|
+
List,
|
|
31
|
+
ListItem,
|
|
32
|
+
Paragraph,
|
|
33
|
+
PhrasingContent,
|
|
34
|
+
Strong,
|
|
35
|
+
Text as MdastText,
|
|
36
|
+
} from 'mdast';
|
|
37
|
+
|
|
38
|
+
import {$createCodeNode, $isCodeNode} from '@lexical/code-core';
|
|
39
|
+
import {$createLinkNode, $isAutoLinkNode, $isLinkNode} from '@lexical/link';
|
|
40
|
+
import {
|
|
41
|
+
$createListItemNode,
|
|
42
|
+
$createListNode,
|
|
43
|
+
$isListItemNode,
|
|
44
|
+
$isListNode,
|
|
45
|
+
} from '@lexical/list';
|
|
46
|
+
import {
|
|
47
|
+
$createHeadingNode,
|
|
48
|
+
$createQuoteNode,
|
|
49
|
+
$isHeadingNode,
|
|
50
|
+
$isQuoteNode,
|
|
51
|
+
} from '@lexical/rich-text';
|
|
52
|
+
import {
|
|
53
|
+
$createLineBreakNode,
|
|
54
|
+
$createParagraphNode,
|
|
55
|
+
$createTextNode,
|
|
56
|
+
$getState,
|
|
57
|
+
$isDecoratorNode,
|
|
58
|
+
$isElementNode,
|
|
59
|
+
$isLineBreakNode,
|
|
60
|
+
$isParagraphNode,
|
|
61
|
+
$isTabNode,
|
|
62
|
+
$isTextNode,
|
|
63
|
+
$setState,
|
|
64
|
+
TEXT_TYPE_TO_FORMAT,
|
|
65
|
+
} from 'lexical';
|
|
66
|
+
|
|
67
|
+
import {
|
|
68
|
+
codeFenceState,
|
|
69
|
+
codeMetaState,
|
|
70
|
+
emphasisMarkerState,
|
|
71
|
+
hardLineBreakState,
|
|
72
|
+
linkStyleState,
|
|
73
|
+
listMarkerState,
|
|
74
|
+
orderedMarkerState,
|
|
75
|
+
paragraphBreakState,
|
|
76
|
+
setextState,
|
|
77
|
+
strongMarkerState,
|
|
78
|
+
} from './state';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Appends `nodes` to `element` via {@link ElementNode.splice} (the primitive
|
|
82
|
+
* `append` delegates to, taking an array directly), returning the element.
|
|
83
|
+
*/
|
|
84
|
+
export function $append<T extends ElementNode>(
|
|
85
|
+
element: T,
|
|
86
|
+
nodes: LexicalNode[],
|
|
87
|
+
): T {
|
|
88
|
+
return element.splice(element.getChildrenSize(), 0, nodes);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prepends `nodes` to `element` via {@link ElementNode.splice} (the primitive
|
|
93
|
+
* `append` delegates to, taking an array directly), returning the element.
|
|
94
|
+
*/
|
|
95
|
+
export function $prepend<T extends ElementNode>(
|
|
96
|
+
element: T,
|
|
97
|
+
nodes: LexicalNode[],
|
|
98
|
+
): T {
|
|
99
|
+
return element.splice(0, 0, nodes);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether `node` is a block-level node: a non-inline element *or* a non-inline
|
|
104
|
+
* decorator (e.g. a horizontal rule).
|
|
105
|
+
*/
|
|
106
|
+
export function $isBlockLevelNode(node: LexicalNode): boolean {
|
|
107
|
+
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A `LineBreakNode` marking a paragraph boundary inside a container. */
|
|
111
|
+
function $createParagraphBreakNode(): LineBreakNode {
|
|
112
|
+
return $setState($createLineBreakNode(), paragraphBreakState, true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reads the first source character of `node` (an inline delimiter, if any). */
|
|
116
|
+
function inlineMarker(
|
|
117
|
+
ctx: MdastImportContext,
|
|
118
|
+
node: {position?: {start: {offset?: number}}},
|
|
119
|
+
): string | undefined {
|
|
120
|
+
if (!ctx.source || !node.position || node.position.start.offset == null) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return ctx.source[node.position.start.offset];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const FORMAT_BOLD = TEXT_TYPE_TO_FORMAT.bold;
|
|
127
|
+
const FORMAT_ITALIC = TEXT_TYPE_TO_FORMAT.italic;
|
|
128
|
+
const FORMAT_STRIKETHROUGH = TEXT_TYPE_TO_FORMAT.strikethrough;
|
|
129
|
+
const FORMAT_CODE = TEXT_TYPE_TO_FORMAT.code;
|
|
130
|
+
|
|
131
|
+
export const TEXT_FORMAT_MASK =
|
|
132
|
+
FORMAT_BOLD | FORMAT_ITALIC | FORMAT_STRIKETHROUGH | FORMAT_CODE;
|
|
133
|
+
|
|
134
|
+
/* -------------------------------------------------------------------------- *
|
|
135
|
+
* Import handlers: mdast node -> Lexical node(s) *
|
|
136
|
+
* -------------------------------------------------------------------------- */
|
|
137
|
+
|
|
138
|
+
export const $importParagraph: MdastImportHandler<Paragraph> = (node, ctx) =>
|
|
139
|
+
$append($createParagraphNode(), ctx.importChildren(node));
|
|
140
|
+
|
|
141
|
+
export const $importHeading: MdastImportHandler<Heading> = (node, ctx) => {
|
|
142
|
+
const heading = $createHeadingNode(`h${node.depth}`);
|
|
143
|
+
// A level 1/2 heading that does not start with a valid ATX marker (`#`..
|
|
144
|
+
// `######` followed by whitespace or end of line) was written in setext
|
|
145
|
+
// style. Checking for the trailing boundary matters: `#foo\n===` is a
|
|
146
|
+
// setext heading whose *content* begins with `#`.
|
|
147
|
+
if (ctx.source && node.position && (node.depth === 1 || node.depth === 2)) {
|
|
148
|
+
const offset = node.position.start.offset;
|
|
149
|
+
if (
|
|
150
|
+
offset != null &&
|
|
151
|
+
!/^ {0,3}#{1,6}([ \t\r\n]|$)/.test(ctx.source.slice(offset, offset + 10))
|
|
152
|
+
) {
|
|
153
|
+
$setState(heading, setextState, true);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return $append(heading, ctx.importChildren(node));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const $importBlockquote: MdastImportHandler<Blockquote> = (
|
|
160
|
+
node,
|
|
161
|
+
ctx,
|
|
162
|
+
) => {
|
|
163
|
+
const quote = $createQuoteNode();
|
|
164
|
+
const children: LexicalNode[] = [];
|
|
165
|
+
for (const child of node.children) {
|
|
166
|
+
if (child.type === 'paragraph') {
|
|
167
|
+
if (children.length > 0) {
|
|
168
|
+
children.push($createParagraphBreakNode());
|
|
169
|
+
}
|
|
170
|
+
children.push(...ctx.importChildren(child));
|
|
171
|
+
} else {
|
|
172
|
+
// Nested blocks (lists, code, nested quotes) are imported as-is so they
|
|
173
|
+
// are not silently flattened to text.
|
|
174
|
+
children.push(...ctx.importNode(child));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return $append(quote, children);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Imports each mdast flow child of `parent` as block-level Lexical nodes,
|
|
182
|
+
* wrapping any stray inline output (e.g. from the `html` fallback) in a
|
|
183
|
+
* paragraph — the same normalization the top-level importer applies.
|
|
184
|
+
*/
|
|
185
|
+
export function $importBlockChildren(
|
|
186
|
+
parent: MdastParent,
|
|
187
|
+
ctx: MdastImportContext,
|
|
188
|
+
): LexicalNode[] {
|
|
189
|
+
const blocks: LexicalNode[] = [];
|
|
190
|
+
let pendingParagraph: ElementNode | null = null;
|
|
191
|
+
const flushPending = () => {
|
|
192
|
+
if (pendingParagraph) {
|
|
193
|
+
blocks.push(pendingParagraph);
|
|
194
|
+
pendingParagraph = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
for (const child of parent.children) {
|
|
198
|
+
for (const node of ctx.importNode(child)) {
|
|
199
|
+
if ($isBlockLevelNode(node)) {
|
|
200
|
+
flushPending();
|
|
201
|
+
blocks.push(node);
|
|
202
|
+
} else {
|
|
203
|
+
if (!pendingParagraph) {
|
|
204
|
+
pendingParagraph = $createParagraphNode();
|
|
205
|
+
}
|
|
206
|
+
$append(pendingParagraph, [node]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
flushPending();
|
|
211
|
+
return blocks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Opt-in replacement for {@link $importBlockquote} that imports the quote as a
|
|
216
|
+
* shadow root {@link QuoteNode} holding block-level children, so structured
|
|
217
|
+
* blockquotes (multiple paragraphs, nested lists, code) round-trip without
|
|
218
|
+
* being flattened to inline content. See `MdastShadowRootQuoteExtension`.
|
|
219
|
+
*/
|
|
220
|
+
export const $importShadowRootBlockquote: MdastImportHandler<Blockquote> = (
|
|
221
|
+
node,
|
|
222
|
+
ctx,
|
|
223
|
+
) =>
|
|
224
|
+
$append(
|
|
225
|
+
$createQuoteNode({shadowRoot: true}),
|
|
226
|
+
$importBlockChildren(node, ctx),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
/** Maps an mdast `list` node to the Lexical {@link ListType} it represents. */
|
|
230
|
+
export function $listTypeFromMdast(node: List): ListType {
|
|
231
|
+
if (node.ordered) {
|
|
232
|
+
return 'number';
|
|
233
|
+
}
|
|
234
|
+
for (const child of node.children) {
|
|
235
|
+
if (child.type === 'listItem' && child.checked != null) {
|
|
236
|
+
return 'check';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return 'bullet';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const $importList: MdastImportHandler<List> = (node, ctx) => {
|
|
243
|
+
const listType = $listTypeFromMdast(node);
|
|
244
|
+
const start = node.ordered && node.start != null ? node.start : 1;
|
|
245
|
+
const list = $createListNode(listType, start);
|
|
246
|
+
// Preserve the literal marker the list used so export can reproduce it.
|
|
247
|
+
// A bounded window is enough: an ordered marker is at most 9 digits plus
|
|
248
|
+
// the delimiter (CommonMark), a bullet is a single character.
|
|
249
|
+
const firstItem = node.children[0];
|
|
250
|
+
const itemOffset =
|
|
251
|
+
ctx.source && firstItem && firstItem.position
|
|
252
|
+
? firstItem.position.start.offset
|
|
253
|
+
: undefined;
|
|
254
|
+
if (itemOffset != null) {
|
|
255
|
+
const markerWindow = ctx.source.slice(itemOffset, itemOffset + 16);
|
|
256
|
+
if (listType === 'number') {
|
|
257
|
+
const match = markerWindow.match(/^\s*\d+([.)])/);
|
|
258
|
+
const delimiter = match && match[1];
|
|
259
|
+
if (delimiter === '.' || delimiter === ')') {
|
|
260
|
+
$setState(list, orderedMarkerState, delimiter);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
const match = markerWindow.match(/^\s*([-*+])/);
|
|
264
|
+
const bullet = match && match[1];
|
|
265
|
+
if (bullet === '-' || bullet === '*' || bullet === '+') {
|
|
266
|
+
$setState(list, listMarkerState, bullet);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return $append(
|
|
271
|
+
list,
|
|
272
|
+
node.children.flatMap(child => ctx.importNode(child)),
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const $importListItem: MdastImportHandler<ListItem> = (node, ctx) => {
|
|
277
|
+
const item = $createListItemNode(
|
|
278
|
+
typeof node.checked === 'boolean' ? node.checked : undefined,
|
|
279
|
+
);
|
|
280
|
+
const extraItems: ListItemNode[] = [];
|
|
281
|
+
for (const child of node.children) {
|
|
282
|
+
if (child.type === 'list') {
|
|
283
|
+
// A nested list is represented in Lexical as a ListItemNode whose only
|
|
284
|
+
// child is the nested ListNode, appended as a sibling of this item.
|
|
285
|
+
extraItems.push($append($createListItemNode(), ctx.importNode(child)));
|
|
286
|
+
} else if (child.type === 'paragraph') {
|
|
287
|
+
if (item.getChildrenSize() > 0) {
|
|
288
|
+
$append(item, [$createParagraphBreakNode()]);
|
|
289
|
+
}
|
|
290
|
+
$append(item, ctx.importChildren(child));
|
|
291
|
+
} else {
|
|
292
|
+
$append(item, ctx.importNode(child));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return [item, ...extraItems];
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const $importCode: MdastImportHandler<Code> = (node, ctx) => {
|
|
299
|
+
const code = $createCodeNode(node.lang || undefined);
|
|
300
|
+
// Preserve the literal fence (e.g. ``` vs ~~~ vs ````) for round-tripping.
|
|
301
|
+
// The fence is on the construct's first line; bound the scan to it.
|
|
302
|
+
if (ctx.source && node.position && node.position.start.offset != null) {
|
|
303
|
+
const offset = node.position.start.offset;
|
|
304
|
+
const lineEnd = ctx.source.indexOf('\n', offset);
|
|
305
|
+
const line = ctx.source.slice(offset, lineEnd === -1 ? undefined : lineEnd);
|
|
306
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
|
|
307
|
+
if (match) {
|
|
308
|
+
$setState(code, codeFenceState, match[1]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// CodeNode only models the language; keep the rest of the info string
|
|
312
|
+
// (` ```js title=x `) as node state so it survives the round-trip.
|
|
313
|
+
if (node.meta) {
|
|
314
|
+
$setState(code, codeMetaState, node.meta);
|
|
315
|
+
}
|
|
316
|
+
if (node.value) {
|
|
317
|
+
$append(code, [$createTextNode(node.value)]);
|
|
318
|
+
}
|
|
319
|
+
return code;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export const importText: MdastImportHandler<MdastText> = (node, ctx) =>
|
|
323
|
+
ctx.createText(node.value);
|
|
324
|
+
|
|
325
|
+
export const importHtml: MdastImportHandler<Html> = (node, ctx) =>
|
|
326
|
+
ctx.createText(node.value);
|
|
327
|
+
|
|
328
|
+
export const importInlineCode: MdastImportHandler<InlineCode> = (node, ctx) =>
|
|
329
|
+
ctx.createText(node.value, ctx.format | FORMAT_CODE);
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Builds the emphasis/strong import handler: applies the format bit and
|
|
333
|
+
* records an underscore delimiter (`_em_` / `__b__`) on the resulting text
|
|
334
|
+
* nodes; `*` is the default and isn't stored.
|
|
335
|
+
*/
|
|
336
|
+
function makeEmphasisImporter(
|
|
337
|
+
format: number,
|
|
338
|
+
markerState: typeof emphasisMarkerState | typeof strongMarkerState,
|
|
339
|
+
): MdastImportHandler<Emphasis | Strong> {
|
|
340
|
+
return (node, ctx) => {
|
|
341
|
+
const children = ctx.importChildren(node, format);
|
|
342
|
+
if (inlineMarker(ctx, node) === '_') {
|
|
343
|
+
for (const child of children) {
|
|
344
|
+
if ($isTextNode(child)) {
|
|
345
|
+
$setState(child, markerState, '_');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return children;
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const $importEmphasis: MdastImportHandler<Emphasis | Strong> =
|
|
354
|
+
/* @__PURE__ */ makeEmphasisImporter(FORMAT_ITALIC, emphasisMarkerState);
|
|
355
|
+
|
|
356
|
+
export const $importStrong: MdastImportHandler<Emphasis | Strong> =
|
|
357
|
+
/* @__PURE__ */ makeEmphasisImporter(FORMAT_BOLD, strongMarkerState);
|
|
358
|
+
|
|
359
|
+
export const importDelete: MdastImportHandler<Delete> = (node, ctx) =>
|
|
360
|
+
ctx.importChildren(node, FORMAT_STRIKETHROUGH);
|
|
361
|
+
|
|
362
|
+
export const $importBreak: MdastImportHandler<Break> = (node, ctx) => {
|
|
363
|
+
// An mdast `break` is always a HARD break; preserve whether it was written
|
|
364
|
+
// as `\` or as trailing spaces, defaulting to `\` when the literal cannot
|
|
365
|
+
// be recovered. (Soft breaks are newlines inside text values and stay
|
|
366
|
+
// unmarked, serializing back to a plain newline.)
|
|
367
|
+
let marker = '\\';
|
|
368
|
+
if (ctx.source && node.position) {
|
|
369
|
+
const {start, end} = node.position;
|
|
370
|
+
if (start.offset != null && end.offset != null) {
|
|
371
|
+
const raw = ctx.source.slice(start.offset, end.offset).replace(/\n$/, '');
|
|
372
|
+
if (/^ {2,}$/.test(raw)) {
|
|
373
|
+
marker = raw;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return [$setState($createLineBreakNode(), hardLineBreakState, marker)];
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const $importLink: MdastImportHandler<Link> = (node, ctx) => {
|
|
381
|
+
const link = $append(
|
|
382
|
+
$createLinkNode(node.url, {
|
|
383
|
+
title: node.title == null ? undefined : node.title,
|
|
384
|
+
}),
|
|
385
|
+
ctx.importChildren(node),
|
|
386
|
+
);
|
|
387
|
+
// Preserve the syntax the link was written in (`[text](url)` vs `<url>`
|
|
388
|
+
// vs a bare GFM autolink literal) so it round-trips unchanged.
|
|
389
|
+
if (ctx.source && node.position && node.position.start.offset != null) {
|
|
390
|
+
const first = ctx.source[node.position.start.offset];
|
|
391
|
+
$setState(
|
|
392
|
+
link,
|
|
393
|
+
linkStyleState,
|
|
394
|
+
first === '[' ? 'inline' : first === '<' ? 'autolink' : 'literal',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return link;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* CommonMark reference links (`[text][id]`, `[id][]`, `[id]`) resolve against
|
|
402
|
+
* the document's definitions. An unresolved reference is literal text per the
|
|
403
|
+
* spec, so it is re-emitted verbatim.
|
|
404
|
+
*/
|
|
405
|
+
export const $importLinkReference: MdastImportHandler<LinkReference> = (
|
|
406
|
+
node,
|
|
407
|
+
ctx,
|
|
408
|
+
) => {
|
|
409
|
+
const definition = ctx.getDefinition(node.identifier);
|
|
410
|
+
if (definition) {
|
|
411
|
+
return $append(
|
|
412
|
+
$createLinkNode(definition.url, {
|
|
413
|
+
title: definition.title == null ? undefined : definition.title,
|
|
414
|
+
}),
|
|
415
|
+
ctx.importChildren(node),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const {position} = node;
|
|
419
|
+
if (ctx.source && position && position.start.offset != null) {
|
|
420
|
+
return ctx.createText(
|
|
421
|
+
ctx.source.slice(position.start.offset, position.end.offset),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
// Approximate the literal when the source is unavailable.
|
|
425
|
+
return [
|
|
426
|
+
...ctx.createText('['),
|
|
427
|
+
...ctx.importChildren(node),
|
|
428
|
+
...ctx.createText(']'),
|
|
429
|
+
];
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Definitions (`[id]: url "title"`) are consumed by {@link collectDefinitions}
|
|
434
|
+
* before the walk; the node itself produces no content.
|
|
435
|
+
*/
|
|
436
|
+
export const importDefinition: MdastImportHandler = () => [];
|
|
437
|
+
|
|
438
|
+
/* -------------------------------------------------------------------------- *
|
|
439
|
+
* Export handlers: Lexical node -> mdast node(s) *
|
|
440
|
+
* -------------------------------------------------------------------------- */
|
|
441
|
+
|
|
442
|
+
export const exportParagraph: MdastExportHandler = (node, ctx) => {
|
|
443
|
+
if (!$isParagraphNode(node)) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
children: ctx.exportInline(node),
|
|
448
|
+
type: 'paragraph',
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const HEADING_DEPTHS: Record<HeadingTagType, Heading['depth']> = {
|
|
453
|
+
h1: 1,
|
|
454
|
+
h2: 2,
|
|
455
|
+
h3: 3,
|
|
456
|
+
h4: 4,
|
|
457
|
+
h5: 5,
|
|
458
|
+
h6: 6,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export const $exportHeading: MdastExportHandler = (node, ctx) => {
|
|
462
|
+
if (!$isHeadingNode(node)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const heading: Heading = {
|
|
466
|
+
children: ctx.exportInline(node),
|
|
467
|
+
depth: HEADING_DEPTHS[node.getTag()],
|
|
468
|
+
type: 'heading',
|
|
469
|
+
};
|
|
470
|
+
// Only pin the style when it is known; nodes created in the editor defer
|
|
471
|
+
// to the document-level serialization options.
|
|
472
|
+
if ($getState(node, setextState)) {
|
|
473
|
+
heading.data = {mdastSetext: true};
|
|
474
|
+
}
|
|
475
|
+
return heading;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const exportQuote: MdastExportHandler = (node, ctx) => {
|
|
479
|
+
if (!$isQuoteNode(node)) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
// A shadow root quote holds block-level children that dispatch directly;
|
|
483
|
+
// a legacy quote holds inline content that must be reassembled into
|
|
484
|
+
// paragraphs. Branching per node lets both forms coexist in one document.
|
|
485
|
+
// Children of a shadow root quote dispatch through the registry, which
|
|
486
|
+
// erases types; block-level output is the dispatch contract here.
|
|
487
|
+
return {
|
|
488
|
+
children: node.isShadowRoot()
|
|
489
|
+
? (ctx.exportChildren(node) as Blockquote['children'])
|
|
490
|
+
: ctx.exportBlocks(node),
|
|
491
|
+
type: 'blockquote',
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export const $exportCode: MdastExportHandler = node => {
|
|
496
|
+
if (!$isCodeNode(node)) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const code: Code = {
|
|
500
|
+
lang: node.getLanguage() || null,
|
|
501
|
+
type: 'code',
|
|
502
|
+
value: node.getTextContent(),
|
|
503
|
+
};
|
|
504
|
+
// The info-string tail is a first-class mdast field; to-markdown emits it
|
|
505
|
+
// after the language (and forces fenced style).
|
|
506
|
+
const meta = $getState(node, codeMetaState);
|
|
507
|
+
if (meta) {
|
|
508
|
+
code.meta = meta;
|
|
509
|
+
}
|
|
510
|
+
// `data.mdastFence` is read back by the exporter's to-markdown wrapper.
|
|
511
|
+
// Only pin the fence when known; editor-created code blocks defer to the
|
|
512
|
+
// document-level serialization options.
|
|
513
|
+
const fence = $getState(node, codeFenceState);
|
|
514
|
+
if (fence) {
|
|
515
|
+
code.data = {mdastFence: fence};
|
|
516
|
+
}
|
|
517
|
+
return code;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const $exportLink: MdastExportHandler = (node, ctx) => {
|
|
521
|
+
if (!$isLinkNode(node) || $isAutoLinkNode(node)) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const link: Link = {
|
|
525
|
+
children: ctx.exportInline(node),
|
|
526
|
+
title: node.getTitle() ?? null,
|
|
527
|
+
type: 'link',
|
|
528
|
+
url: node.getURL(),
|
|
529
|
+
};
|
|
530
|
+
// Only pinned when known — editor-created links defer to the default
|
|
531
|
+
// serialization (autolink form when the text is the URL).
|
|
532
|
+
const style = $getState(node, linkStyleState);
|
|
533
|
+
if (style) {
|
|
534
|
+
link.data = {mdastLinkStyle: style};
|
|
535
|
+
}
|
|
536
|
+
return link;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
function $exportListNode(node: ListNode, ctx: MdastExportContext): List {
|
|
540
|
+
const listType = node.getListType();
|
|
541
|
+
const list: List = {
|
|
542
|
+
children: [],
|
|
543
|
+
ordered: listType === 'number',
|
|
544
|
+
spread: false,
|
|
545
|
+
start: listType === 'number' ? node.getStart() : undefined,
|
|
546
|
+
type: 'list',
|
|
547
|
+
};
|
|
548
|
+
// Preserve the marker the list used; read back by the exporter's
|
|
549
|
+
// to-markdown wrapper. Only pinned when known — editor-created lists defer
|
|
550
|
+
// to the document-level serialization options.
|
|
551
|
+
if (listType === 'number') {
|
|
552
|
+
const marker = $getState(node, orderedMarkerState);
|
|
553
|
+
if (marker) {
|
|
554
|
+
list.data = {mdastBulletOrdered: marker};
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
const marker = $getState(node, listMarkerState);
|
|
558
|
+
if (marker) {
|
|
559
|
+
list.data = {mdastBullet: marker};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
let previousItem: ListItem | null = null;
|
|
563
|
+
for (const child of node.getChildren()) {
|
|
564
|
+
// Structural iteration bypasses the walk's selection filter, so items a
|
|
565
|
+
// selection export does not reach are skipped here.
|
|
566
|
+
if (!$isListItemNode(child) || !ctx.isIncluded(child)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const firstChild = child.getFirstChild();
|
|
570
|
+
// A list item whose sole child is a nested list represents nesting: attach
|
|
571
|
+
// the nested list to the previous item's children.
|
|
572
|
+
if (child.getChildrenSize() === 1 && $isListNode(firstChild)) {
|
|
573
|
+
const nested = $exportListNode(firstChild, ctx);
|
|
574
|
+
if (previousItem) {
|
|
575
|
+
previousItem.children.push(nested);
|
|
576
|
+
} else {
|
|
577
|
+
list.children.push({
|
|
578
|
+
children: [nested],
|
|
579
|
+
spread: false,
|
|
580
|
+
type: 'listItem',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const item: ListItem = {
|
|
586
|
+
checked: listType === 'check' ? (child.getChecked() ?? false) : null,
|
|
587
|
+
children: ctx.exportBlocks(child),
|
|
588
|
+
spread: false,
|
|
589
|
+
type: 'listItem',
|
|
590
|
+
};
|
|
591
|
+
list.children.push(item);
|
|
592
|
+
previousItem = item;
|
|
593
|
+
}
|
|
594
|
+
return list;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export const $exportList: MdastExportHandler = (node, ctx) => {
|
|
598
|
+
if (!$isListNode(node)) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
return $exportListNode(node, ctx);
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Wraps a plain string in the mdast phrasing nodes implied by a Lexical text
|
|
606
|
+
* format bitmask (code span innermost, then emphasis, strong, strikethrough).
|
|
607
|
+
* The emphasis/strong *delimiter* (`*` vs `_`) is a document-level to-markdown
|
|
608
|
+
* option (see the exporter) rather than per-node, because mixing delimiters in
|
|
609
|
+
* one document desyncs to-markdown's character escaping.
|
|
610
|
+
*/
|
|
611
|
+
export function phrasingFromFormattedText(
|
|
612
|
+
value: string,
|
|
613
|
+
format: number,
|
|
614
|
+
): PhrasingContent {
|
|
615
|
+
let content: PhrasingContent =
|
|
616
|
+
format & FORMAT_CODE ? {type: 'inlineCode', value} : {type: 'text', value};
|
|
617
|
+
if (format & FORMAT_ITALIC) {
|
|
618
|
+
content = {children: [content], type: 'emphasis'};
|
|
619
|
+
}
|
|
620
|
+
if (format & FORMAT_BOLD) {
|
|
621
|
+
content = {children: [content], type: 'strong'};
|
|
622
|
+
}
|
|
623
|
+
if (format & FORMAT_STRIKETHROUGH) {
|
|
624
|
+
content = {children: [content], type: 'delete'};
|
|
625
|
+
}
|
|
626
|
+
return content;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export const exportText = (node: LexicalNode): PhrasingContent | null => {
|
|
630
|
+
if (!$isTextNode(node)) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
return phrasingFromFormattedText(
|
|
634
|
+
node.getTextContent(),
|
|
635
|
+
node.getFormat() & TEXT_FORMAT_MASK,
|
|
636
|
+
);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
export const $exportLineBreak = (node: LexicalNode): Break | null =>
|
|
640
|
+
$isLineBreakNode(node)
|
|
641
|
+
? {
|
|
642
|
+
data: {mdastBreak: $getState(node, hardLineBreakState)},
|
|
643
|
+
type: 'break',
|
|
644
|
+
}
|
|
645
|
+
: null;
|
|
646
|
+
|
|
647
|
+
export const exportTab = (node: LexicalNode): MdastText | null =>
|
|
648
|
+
$isTabNode(node) ? {type: 'text', value: '\t'} : null;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
// `@lexical/mdast` is configured exclusively through the Lexical extension
|
|
10
|
+
// system. Each feature extension ships the nodes it needs and contributes its
|
|
11
|
+
// import/export rules (and micromark/mdast extensions) to the core
|
|
12
|
+
// `MdastImportExtension` registry — mirroring how `@lexical/html`'s feature
|
|
13
|
+
// extensions contribute to `DOMImportExtension`.
|
|
14
|
+
//
|
|
15
|
+
// - Add `MdastCommonMarkExtension` (or individual feature extensions) for
|
|
16
|
+
// import, `MdastExportExtension` to serialize back to Markdown (or
|
|
17
|
+
// `MdastExtension`, which bundles both directions), and
|
|
18
|
+
// `MdastShortcutsExtension` for streaming shortcuts.
|
|
19
|
+
// - Read the Markdown API from the editor with
|
|
20
|
+
// `$getExtensionOutput(MdastImportExtension)` /
|
|
21
|
+
// `$getExtensionOutput(MdastExportExtension)`, or via the
|
|
22
|
+
// `$convert*` shorthands.
|
|
23
|
+
|
|
24
|
+
export type {MdastExportExtensionOutput} from './MdastExportExtension';
|
|
25
|
+
export {
|
|
26
|
+
$convertSelectionToMarkdownString,
|
|
27
|
+
$convertToMarkdownString,
|
|
28
|
+
$convertToMdast,
|
|
29
|
+
MdastExportExtension,
|
|
30
|
+
} from './MdastExportExtension';
|
|
31
|
+
export {MdastExtension} from './MdastExtension';
|
|
32
|
+
export {MdastGfmExtension} from './MdastGfmExtension';
|
|
33
|
+
export type {
|
|
34
|
+
MdastConfig,
|
|
35
|
+
MdastImportExtensionOutput,
|
|
36
|
+
MdastShortcutsConfig,
|
|
37
|
+
} from './MdastImportExtension';
|
|
38
|
+
export {
|
|
39
|
+
$convertFromMarkdownString,
|
|
40
|
+
$convertFromMdast,
|
|
41
|
+
$generateNodesFromMarkdownString,
|
|
42
|
+
$generateNodesFromMdast,
|
|
43
|
+
MdastAutolinkLiteralExtension,
|
|
44
|
+
MdastBlockquoteExtension,
|
|
45
|
+
MdastCodeExtension,
|
|
46
|
+
MdastCommonMarkExtension,
|
|
47
|
+
MdastHeadingExtension,
|
|
48
|
+
MdastHorizontalRuleExtension,
|
|
49
|
+
MdastImportExtension,
|
|
50
|
+
MdastLinkExtension,
|
|
51
|
+
MdastListExtension,
|
|
52
|
+
MdastRichTextExtension,
|
|
53
|
+
MdastShadowRootQuoteExtension,
|
|
54
|
+
MdastShortcutsExtension,
|
|
55
|
+
MdastStrikethroughExtension,
|
|
56
|
+
MdastTaskListExtension,
|
|
57
|
+
} from './MdastImportExtension';
|
|
58
|
+
export {MdastTableExtension} from './MdastTableExtension';
|
|
59
|
+
export type {
|
|
60
|
+
CompiledMdast,
|
|
61
|
+
MdastExportContext,
|
|
62
|
+
MdastExportHandler,
|
|
63
|
+
MdastExportRule,
|
|
64
|
+
MdastImportContext,
|
|
65
|
+
MdastImportHandler,
|
|
66
|
+
MdastImportRule,
|
|
67
|
+
MdastNode,
|
|
68
|
+
MdastParent,
|
|
69
|
+
} from './types';
|