@lexical/mdast 0.0.0-bootstrap.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }