@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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {defineExtension} from 'lexical';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
MdastAutolinkLiteralExtension,
|
|
13
|
+
MdastStrikethroughExtension,
|
|
14
|
+
MdastTaskListExtension,
|
|
15
|
+
} from './MdastImportExtension';
|
|
16
|
+
import {MdastTableExtension} from './MdastTableExtension';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convenience bundle of every GFM extension — strikethrough, task lists,
|
|
20
|
+
* literal autolinks, and tables — mirroring the scope of
|
|
21
|
+
* `micromark-extension-gfm`. Combine with `MdastCommonMarkExtension` for
|
|
22
|
+
* GitHub-flavored Markdown:
|
|
23
|
+
* ```ts
|
|
24
|
+
* dependencies: [MdastCommonMarkExtension, MdastGfmExtension]
|
|
25
|
+
* ```
|
|
26
|
+
* Each member is also usable individually when you only want some of GFM
|
|
27
|
+
* (e.g. task lists without tables).
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
export const MdastGfmExtension = /* @__PURE__ */ defineExtension({
|
|
31
|
+
dependencies: [
|
|
32
|
+
MdastStrikethroughExtension,
|
|
33
|
+
MdastTaskListExtension,
|
|
34
|
+
MdastAutolinkLiteralExtension,
|
|
35
|
+
MdastTableExtension,
|
|
36
|
+
],
|
|
37
|
+
name: '@lexical/mdast/Gfm',
|
|
38
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
CompiledMdast,
|
|
11
|
+
MdastImportContext,
|
|
12
|
+
MdastNode,
|
|
13
|
+
MdastParent,
|
|
14
|
+
} from './types';
|
|
15
|
+
import type {ElementNode, LexicalNode} from 'lexical';
|
|
16
|
+
import type {Root} from 'mdast';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
$createLineBreakNode,
|
|
20
|
+
$createParagraphNode,
|
|
21
|
+
$createTabNode,
|
|
22
|
+
$createTextNode,
|
|
23
|
+
$getRoot,
|
|
24
|
+
$getSelection,
|
|
25
|
+
tokenizeRawText,
|
|
26
|
+
} from 'lexical';
|
|
27
|
+
import {fromMarkdown} from 'mdast-util-from-markdown';
|
|
28
|
+
|
|
29
|
+
import {$append, $isBlockLevelNode, $prepend} from './handlers';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Splits `value` into a run of `TextNode`s via {@link tokenizeRawText}:
|
|
33
|
+
* `\n` becomes a `LineBreakNode` and `\t` a `TabNode` (matching how typed
|
|
34
|
+
* content is represented in the editor), with `format` applied to each text
|
|
35
|
+
* segment. Empty segments are dropped so a leading/trailing/standalone
|
|
36
|
+
* separator yields only its node.
|
|
37
|
+
*/
|
|
38
|
+
function $createTextNodes(value: string, format: number): LexicalNode[] {
|
|
39
|
+
const out: LexicalNode[] = [];
|
|
40
|
+
tokenizeRawText(value, {
|
|
41
|
+
linebreak: () => out.push($createLineBreakNode()),
|
|
42
|
+
tab: () => out.push($createTabNode()),
|
|
43
|
+
text: segment => {
|
|
44
|
+
const textNode = $createTextNode(segment);
|
|
45
|
+
if (format) {
|
|
46
|
+
textNode.setFormat(format);
|
|
47
|
+
}
|
|
48
|
+
out.push(textNode);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A resolved `[identifier]: url "title"` definition. */
|
|
55
|
+
export type ResolvedDefinition = {url: string; title?: string | null};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collects the document's definitions (`[id]: url "title"`) so link/image
|
|
59
|
+
* references can be resolved during the import walk. Identifiers on mdast
|
|
60
|
+
* `definition` nodes are already normalized.
|
|
61
|
+
*/
|
|
62
|
+
export function collectDefinitions(
|
|
63
|
+
tree: Root,
|
|
64
|
+
): Map<string, ResolvedDefinition> {
|
|
65
|
+
const definitions = new Map<string, ResolvedDefinition>();
|
|
66
|
+
const visit = (node: MdastNode): void => {
|
|
67
|
+
if (node.type === 'definition') {
|
|
68
|
+
// CommonMark: the FIRST definition of an identifier wins.
|
|
69
|
+
if (!definitions.has(node.identifier)) {
|
|
70
|
+
definitions.set(node.identifier, {title: node.title, url: node.url});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if ('children' in node) {
|
|
74
|
+
for (const child of node.children) {
|
|
75
|
+
visit(child);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
visit(tree);
|
|
80
|
+
return definitions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const NO_DEFINITIONS: ReadonlyMap<string, ResolvedDefinition> = new Map();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Builds the recursive importer for a compiled set of transformers. The
|
|
87
|
+
* returned function converts a single mdast node into Lexical nodes, threading
|
|
88
|
+
* the accumulated text-format bitmask through inline marks.
|
|
89
|
+
*
|
|
90
|
+
* Exported so the streaming shortcut engine can reuse the exact same mdast ->
|
|
91
|
+
* Lexical mapping when materializing an inline construct it detected.
|
|
92
|
+
*/
|
|
93
|
+
export function createNodeImporter(
|
|
94
|
+
compiled: CompiledMdast,
|
|
95
|
+
source = '',
|
|
96
|
+
definitions: ReadonlyMap<string, ResolvedDefinition> = NO_DEFINITIONS,
|
|
97
|
+
) {
|
|
98
|
+
const {importHandlers} = compiled;
|
|
99
|
+
// The context only depends on the accumulated format bitmask, which takes a
|
|
100
|
+
// handful of distinct values per document — cache instead of allocating one
|
|
101
|
+
// (plus three closures) per visited node.
|
|
102
|
+
const contextByFormat = new Map<number, MdastImportContext>();
|
|
103
|
+
|
|
104
|
+
function getContext(format: number): MdastImportContext {
|
|
105
|
+
let context = contextByFormat.get(format);
|
|
106
|
+
if (context === undefined) {
|
|
107
|
+
context = {
|
|
108
|
+
createText: (value, fmt) =>
|
|
109
|
+
$createTextNodes(value, fmt == null ? format : fmt),
|
|
110
|
+
format,
|
|
111
|
+
getDefinition: identifier => definitions.get(identifier),
|
|
112
|
+
importChildren: (parent, extra) =>
|
|
113
|
+
$importChildren(parent, format | (extra || 0)),
|
|
114
|
+
importNode: (node, extra) => $importNode(node, format | (extra || 0)),
|
|
115
|
+
source,
|
|
116
|
+
};
|
|
117
|
+
contextByFormat.set(format, context);
|
|
118
|
+
}
|
|
119
|
+
return context;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function $importNode(node: MdastNode, format: number): LexicalNode[] {
|
|
123
|
+
const handler = importHandlers.get(node.type);
|
|
124
|
+
if (handler) {
|
|
125
|
+
const result = handler(node, getContext(format));
|
|
126
|
+
if (result == null) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(result) ? result : [result];
|
|
130
|
+
}
|
|
131
|
+
// Fallback: unwrap unknown containers, render unknown literals as text, and
|
|
132
|
+
// drop anything else so no content silently corrupts the tree.
|
|
133
|
+
if ('children' in node) {
|
|
134
|
+
return $importChildren(node, format);
|
|
135
|
+
}
|
|
136
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
137
|
+
return $createTextNodes(node.value, format);
|
|
138
|
+
}
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function $importChildren(parent: MdastParent, format: number): LexicalNode[] {
|
|
143
|
+
const out: LexicalNode[] = [];
|
|
144
|
+
for (const child of parent.children) {
|
|
145
|
+
out.push(...$importNode(child, format));
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {$importChildren, $importNode};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates the import entry points for a compiled registry. The `Markdown`
|
|
155
|
+
* variants parse a source string (recovering literal syntax like the list
|
|
156
|
+
* bullet or link style from it); the `Mdast` variants walk a pre-parsed
|
|
157
|
+
* tree, where no source string exists so syntax-preservation is skipped.
|
|
158
|
+
* `$generateNodesFrom*` return an array of detached block-level Lexical
|
|
159
|
+
* nodes without touching the document or the selection; `$import*` replace
|
|
160
|
+
* the contents of the root (or a supplied element) with that result.
|
|
161
|
+
*/
|
|
162
|
+
export function createMdastImport(compiled: CompiledMdast): {
|
|
163
|
+
$generateNodesFromMarkdown: (markdown: string) => LexicalNode[];
|
|
164
|
+
$generateNodesFromMdast: (tree: Root) => LexicalNode[];
|
|
165
|
+
$importMarkdown: (markdown: string, node?: ElementNode) => void;
|
|
166
|
+
$importMdast: (tree: Root, node?: ElementNode) => void;
|
|
167
|
+
} {
|
|
168
|
+
const $generateNodes = (tree: Root, source: string): LexicalNode[] => {
|
|
169
|
+
const {$importNode} = createNodeImporter(
|
|
170
|
+
compiled,
|
|
171
|
+
source,
|
|
172
|
+
collectDefinitions(tree),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Top-level mdast children should produce block-level Lexical nodes. Any
|
|
176
|
+
// stray inline content (e.g. from a fallback) is wrapped in a paragraph
|
|
177
|
+
// so the result only ever contains valid block children.
|
|
178
|
+
const blocks: LexicalNode[] = [];
|
|
179
|
+
let pendingParagraph: ElementNode | null = null;
|
|
180
|
+
const flushPending = () => {
|
|
181
|
+
if (pendingParagraph) {
|
|
182
|
+
blocks.push(pendingParagraph);
|
|
183
|
+
pendingParagraph = null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
for (const child of tree.children) {
|
|
187
|
+
for (const lexicalNode of $importNode(child, 0)) {
|
|
188
|
+
if ($isBlockLevelNode(lexicalNode)) {
|
|
189
|
+
flushPending();
|
|
190
|
+
blocks.push(lexicalNode);
|
|
191
|
+
} else {
|
|
192
|
+
if (!pendingParagraph) {
|
|
193
|
+
pendingParagraph = $createParagraphNode();
|
|
194
|
+
}
|
|
195
|
+
$append(pendingParagraph, [lexicalNode]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
flushPending();
|
|
200
|
+
return blocks;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const $generateNodesFromMarkdown = (markdown: string): LexicalNode[] =>
|
|
204
|
+
$generateNodes(
|
|
205
|
+
fromMarkdown(markdown, {
|
|
206
|
+
extensions: compiled.micromarkExtensions,
|
|
207
|
+
mdastExtensions: compiled.mdastExtensions,
|
|
208
|
+
}),
|
|
209
|
+
markdown,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const $generateNodesFromMdast = (tree: Root): LexicalNode[] =>
|
|
213
|
+
$generateNodes(tree, '');
|
|
214
|
+
|
|
215
|
+
const $replaceWithBlocks = (
|
|
216
|
+
blocks: LexicalNode[],
|
|
217
|
+
node?: ElementNode,
|
|
218
|
+
): void => {
|
|
219
|
+
const root = node || $getRoot();
|
|
220
|
+
root.clear();
|
|
221
|
+
$prepend(root, blocks.length > 0 ? blocks : [$createParagraphNode()]);
|
|
222
|
+
|
|
223
|
+
if ($getSelection() !== null) {
|
|
224
|
+
root.selectStart();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
$generateNodesFromMarkdown,
|
|
230
|
+
$generateNodesFromMdast,
|
|
231
|
+
$importMarkdown: (markdown, node) =>
|
|
232
|
+
$replaceWithBlocks($generateNodesFromMarkdown(markdown), node),
|
|
233
|
+
$importMdast: (tree, node) =>
|
|
234
|
+
$replaceWithBlocks($generateNodesFromMdast(tree), node),
|
|
235
|
+
};
|
|
236
|
+
}
|