@blue-patterns/richtext 0.0.0-stage → 0.7.18

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.
Files changed (201) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/LICENSE +22 -0
  3. package/README.md +88 -2
  4. package/astryx.integration.mjs +11 -0
  5. package/blocks/RichTextEditorShowcase.template.mjs +13 -0
  6. package/blocks/RichTextEditorShowcase.tsx +19 -0
  7. package/dist/RichTextEditor.d.ts +282 -0
  8. package/dist/RichTextEditor.d.ts.map +1 -0
  9. package/dist/RichTextEditor.js +657 -0
  10. package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
  11. package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
  12. package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
  13. package/dist/RichTextEditorToolbar.d.ts +186 -0
  14. package/dist/RichTextEditorToolbar.d.ts.map +1 -0
  15. package/dist/RichTextEditorToolbar.js +688 -0
  16. package/dist/RichTextView.d.ts +92 -0
  17. package/dist/RichTextView.d.ts.map +1 -0
  18. package/dist/RichTextView.js +267 -0
  19. package/dist/codeLanguages.d.ts +14 -0
  20. package/dist/codeLanguages.d.ts.map +1 -0
  21. package/dist/codeLanguages.js +34 -0
  22. package/dist/codeLineTokens.d.ts +53 -0
  23. package/dist/codeLineTokens.d.ts.map +1 -0
  24. package/dist/codeLineTokens.js +188 -0
  25. package/dist/editorNodes.d.ts +8 -0
  26. package/dist/editorNodes.d.ts.map +1 -0
  27. package/dist/editorNodes.js +30 -0
  28. package/dist/editorTheme.d.ts +7 -0
  29. package/dist/editorTheme.d.ts.map +1 -0
  30. package/dist/editorTheme.js +328 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
  35. package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
  36. package/dist/internal/CodeBackspacePlugin.js +70 -0
  37. package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
  38. package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
  39. package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
  40. package/dist/internal/CodePastePlugin.d.ts +5 -0
  41. package/dist/internal/CodePastePlugin.d.ts.map +1 -0
  42. package/dist/internal/CodePastePlugin.js +104 -0
  43. package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
  44. package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
  45. package/dist/internal/CodeSyntaxPlugin.js +397 -0
  46. package/dist/internal/CodeTabPlugin.d.ts +2 -0
  47. package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
  48. package/dist/internal/CodeTabPlugin.js +256 -0
  49. package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
  50. package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
  51. package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
  52. package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
  53. package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
  54. package/dist/internal/ImageSourceCaretPlugin.js +105 -0
  55. package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
  56. package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
  57. package/dist/internal/ListItemShortcutsPlugin.js +314 -0
  58. package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
  59. package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
  60. package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
  61. package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
  62. package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
  63. package/dist/internal/SelectAllScopePlugin.js +67 -0
  64. package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
  65. package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
  66. package/dist/internal/TableColumnFloorPlugin.js +84 -0
  67. package/dist/internal/TablePastePlugin.d.ts +9 -0
  68. package/dist/internal/TablePastePlugin.d.ts.map +1 -0
  69. package/dist/internal/TablePastePlugin.js +183 -0
  70. package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
  71. package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
  72. package/dist/internal/TableScrollRegionPlugin.js +95 -0
  73. package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
  74. package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
  75. package/dist/internal/TaskCheckboxPlugin.js +193 -0
  76. package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
  77. package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
  78. package/dist/internal/ToolbarLinkPopover.js +292 -0
  79. package/dist/linkEditing.d.ts +6 -0
  80. package/dist/linkEditing.d.ts.map +1 -0
  81. package/dist/linkEditing.js +20 -0
  82. package/dist/linkForm.d.ts +28 -0
  83. package/dist/linkForm.d.ts.map +1 -0
  84. package/dist/linkForm.js +137 -0
  85. package/dist/linkUtils.d.ts +27 -0
  86. package/dist/linkUtils.d.ts.map +1 -0
  87. package/dist/linkUtils.js +101 -0
  88. package/dist/markdown.d.ts +17 -0
  89. package/dist/markdown.d.ts.map +1 -0
  90. package/dist/markdown.js +17 -0
  91. package/dist/markdownCharacterReferences.d.ts +135 -0
  92. package/dist/markdownCharacterReferences.d.ts.map +1 -0
  93. package/dist/markdownCharacterReferences.js +1127 -0
  94. package/dist/markdownCodeFence.d.ts +4 -0
  95. package/dist/markdownCodeFence.d.ts.map +1 -0
  96. package/dist/markdownCodeFence.js +167 -0
  97. package/dist/markdownExtensionNode.d.ts +88 -0
  98. package/dist/markdownExtensionNode.d.ts.map +1 -0
  99. package/dist/markdownExtensionNode.js +248 -0
  100. package/dist/markdownExtensions.d.ts +115 -0
  101. package/dist/markdownExtensions.d.ts.map +1 -0
  102. package/dist/markdownExtensions.js +392 -0
  103. package/dist/markdownHardLineBreak.d.ts +25 -0
  104. package/dist/markdownHardLineBreak.d.ts.map +1 -0
  105. package/dist/markdownHardLineBreak.js +59 -0
  106. package/dist/markdownHeading.d.ts +20 -0
  107. package/dist/markdownHeading.d.ts.map +1 -0
  108. package/dist/markdownHeading.js +30 -0
  109. package/dist/markdownLink.d.ts +5 -0
  110. package/dist/markdownLink.d.ts.map +1 -0
  111. package/dist/markdownLink.js +80 -0
  112. package/dist/markdownListExport.d.ts +3 -0
  113. package/dist/markdownListExport.d.ts.map +1 -0
  114. package/dist/markdownListExport.js +68 -0
  115. package/dist/markdownListIndentation.d.ts +8 -0
  116. package/dist/markdownListIndentation.d.ts.map +1 -0
  117. package/dist/markdownListIndentation.js +101 -0
  118. package/dist/markdownOrderedList.d.ts +3 -0
  119. package/dist/markdownOrderedList.d.ts.map +1 -0
  120. package/dist/markdownOrderedList.js +51 -0
  121. package/dist/markdownQuote.d.ts +22 -0
  122. package/dist/markdownQuote.d.ts.map +1 -0
  123. package/dist/markdownQuote.js +32 -0
  124. package/dist/markdownSerializers.d.ts +49 -0
  125. package/dist/markdownSerializers.d.ts.map +1 -0
  126. package/dist/markdownSerializers.js +83 -0
  127. package/dist/markdownSource.d.ts +71 -0
  128. package/dist/markdownSource.d.ts.map +1 -0
  129. package/dist/markdownSource.js +804 -0
  130. package/dist/markdownSpaceRuns.d.ts +24 -0
  131. package/dist/markdownSpaceRuns.d.ts.map +1 -0
  132. package/dist/markdownSpaceRuns.js +147 -0
  133. package/dist/markdownTable.d.ts +35 -0
  134. package/dist/markdownTable.d.ts.map +1 -0
  135. package/dist/markdownTable.js +227 -0
  136. package/dist/markdownTaskList.d.ts +48 -0
  137. package/dist/markdownTaskList.d.ts.map +1 -0
  138. package/dist/markdownTaskList.js +166 -0
  139. package/dist/markdownThematicBreak.d.ts +3 -0
  140. package/dist/markdownThematicBreak.d.ts.map +1 -0
  141. package/dist/markdownThematicBreak.js +55 -0
  142. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
  143. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
  144. package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
  145. package/dist/richtext.css +213 -0
  146. package/dist/textSemantics.d.ts +3 -0
  147. package/dist/textSemantics.d.ts.map +1 -0
  148. package/dist/textSemantics.js +87 -0
  149. package/package.json +158 -3
  150. package/src/RichTextEditor.doc.mjs +308 -0
  151. package/src/RichTextEditor.tsx +1109 -0
  152. package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
  153. package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
  154. package/src/RichTextEditorToolbar.doc.mjs +92 -0
  155. package/src/RichTextEditorToolbar.tsx +871 -0
  156. package/src/RichTextView.doc.mjs +101 -0
  157. package/src/RichTextView.tsx +351 -0
  158. package/src/codeLanguages.ts +42 -0
  159. package/src/codeLineTokens.ts +241 -0
  160. package/src/doc-mjs.d.ts +23 -0
  161. package/src/editorNodes.ts +44 -0
  162. package/src/editorTheme.ts +550 -0
  163. package/src/index.ts +59 -0
  164. package/src/internal/CodeBackspacePlugin.tsx +96 -0
  165. package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
  166. package/src/internal/CodePastePlugin.tsx +144 -0
  167. package/src/internal/CodeSyntaxPlugin.tsx +493 -0
  168. package/src/internal/CodeTabPlugin.tsx +298 -0
  169. package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
  170. package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
  171. package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
  172. package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
  173. package/src/internal/SelectAllScopePlugin.tsx +86 -0
  174. package/src/internal/TableColumnFloorPlugin.tsx +99 -0
  175. package/src/internal/TablePastePlugin.tsx +223 -0
  176. package/src/internal/TableScrollRegionPlugin.tsx +107 -0
  177. package/src/internal/TaskCheckboxPlugin.tsx +246 -0
  178. package/src/internal/ToolbarLinkPopover.tsx +392 -0
  179. package/src/linkEditing.ts +20 -0
  180. package/src/linkForm.tsx +156 -0
  181. package/src/linkUtils.ts +106 -0
  182. package/src/markdown.ts +25 -0
  183. package/src/markdownCharacterReferences.ts +1137 -0
  184. package/src/markdownCodeFence.ts +216 -0
  185. package/src/markdownExtensionNode.ts +339 -0
  186. package/src/markdownExtensions.ts +496 -0
  187. package/src/markdownHardLineBreak.ts +73 -0
  188. package/src/markdownHeading.ts +33 -0
  189. package/src/markdownLink.ts +86 -0
  190. package/src/markdownListExport.ts +85 -0
  191. package/src/markdownListIndentation.ts +105 -0
  192. package/src/markdownOrderedList.ts +60 -0
  193. package/src/markdownQuote.ts +35 -0
  194. package/src/markdownSerializers.ts +108 -0
  195. package/src/markdownSource.ts +1045 -0
  196. package/src/markdownSpaceRuns.ts +164 -0
  197. package/src/markdownTable.ts +309 -0
  198. package/src/markdownTaskList.ts +222 -0
  199. package/src/markdownThematicBreak.ts +66 -0
  200. package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
  201. package/src/textSemantics.ts +104 -0
@@ -0,0 +1,392 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownExtensions.ts
5
+ * @input Uses core's server-safe plugin protocol (getMarkdownPluginCapabilities)
6
+ * and parser (parseMarkdownAst), lexical, and RichTextExtensionNode.
7
+ * @output Exports createRichTextExtension, RichTextExtensionError, and the
8
+ * RichTextMarkdownExtension type; and, for RichText's own Markdown import and
9
+ * export, the configuration check, the source shielding and restoring that
10
+ * puts core-recognized plugin nodes into the editor, and the transformers
11
+ * that export them.
12
+ * @position Implements spec:AST-064 FR1–FR6 and FR10 for RichText's Markdown
13
+ * import and export. Server-safe: imports no React, DOM, or client module.
14
+ *
15
+ * A plugin is the entry core's createMarkdownPlugin returns; RichText adopts it
16
+ * as it is (FR1), refusing a plugin that declares a transform (FR4, DEC-3).
17
+ * Before Lexical imports a chunk of Markdown, core's parser finds the adopted
18
+ * plugins' nodes in it, and each node's exact source becomes a private-use
19
+ * stand-in that Lexical's transformers read as plain text (FR5, DEC-4). Core
20
+ * reports where a top-level block node sits but not an inline one, so each
21
+ * node is placed at the occurrence of its source that core itself recognizes:
22
+ * replacing that occurrence, and only that one, removes exactly that node from
23
+ * core's parse. After the import, each stand-in becomes an atomic
24
+ * RichTextExtensionNode holding the node's source and data.
25
+ */
26
+
27
+ import { $createParagraphNode, $createTextNode, $isLineBreakNode, $isParagraphNode, TextNode } from 'lexical';
28
+ import { getMarkdownPluginCapabilities } from '@blue-patterns/core/Markdown/plugins';
29
+ import { parseMarkdownAst } from '@blue-patterns/core/Markdown/parser';
30
+ import { $createRichTextExtensionNode, $isRichTextExtensionNode, RichTextExtensionNode } from "./markdownExtensionNode.js";
31
+ const extensionPlugin = Symbol('RichTextMarkdownExtension.plugin');
32
+
33
+ /**
34
+ * One Markdown plugin adopted by RichText. Opaque: create it with
35
+ * `createRichTextExtension`, and pass it to the surfaces and serializers that
36
+ * should recognize the plugin's syntax.
37
+ */
38
+
39
+ /** Why RichText refused a plugin or a configuration (spec:AST-064 FR4, FR6). */
40
+ export class RichTextExtensionError extends Error {
41
+ /** The plugin RichText refused, or that a configuration claims twice. */
42
+
43
+ /** What RichText cannot honor: `transform`, or what was claimed twice. */
44
+
45
+ /** The transformer or second extension that claims the same thing. */
46
+
47
+ constructor(plugin, capability, claimant) {
48
+ super(claimant == null ? `RichText cannot adopt Markdown plugin "${plugin}": RichText does not run its ${capability}.` : `RichText cannot use Markdown plugin "${plugin}" with ${claimant}: both claim its ${capability}.`);
49
+ this.name = 'RichTextExtensionError';
50
+ this.plugin = plugin;
51
+ this.capability = capability;
52
+ this.claimant = claimant;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Adopts a Markdown plugin for RichText (spec:AST-064 FR1, FR4). RichText
58
+ * recognizes the plugin's syntax with core's parser and keeps each node's
59
+ * source; it never runs the plugin's immutable transform, so a plugin that
60
+ * declares one is refused with a `RichTextExtensionError` here, before any
61
+ * surface uses it.
62
+ *
63
+ * @example
64
+ * ```
65
+ * const mentions = createRichTextExtension(mentionsPlugin);
66
+ * markdownToEditorStateJSON(markdown, {extensions: [mentions]});
67
+ * ```
68
+ */
69
+ export function createRichTextExtension(plugin) {
70
+ const capabilities = getMarkdownPluginCapabilities(plugin);
71
+ if (capabilities.transform) {
72
+ throw new RichTextExtensionError(plugin.name, 'transform', null);
73
+ }
74
+ return Object.freeze({
75
+ name: plugin.name,
76
+ [extensionPlugin]: plugin
77
+ });
78
+ }
79
+
80
+ /**
81
+ * The plugins behind `extensions`, after checking that the configuration is
82
+ * one RichText can honor (spec:AST-064 FR6): no plugin adopted twice, and no
83
+ * transformer that imports or exports plugin nodes.
84
+ */
85
+ export function pluginsOf(extensions, transformers) {
86
+ const seen = new Set();
87
+ for (const extension of extensions) {
88
+ if (seen.has(extension.name)) {
89
+ throw new RichTextExtensionError(extension.name, 'syntax', `a second extension for "${extension.name}"`);
90
+ }
91
+ seen.add(extension.name);
92
+ }
93
+ const [first] = extensions;
94
+ if (first != null) {
95
+ transformers.forEach((transformer, index) => {
96
+ if ('dependencies' in transformer && transformer.dependencies.includes(RichTextExtensionNode)) {
97
+ throw new RichTextExtensionError(first.name, 'nodes', `transformer ${index} (${transformer.type})`);
98
+ }
99
+ });
100
+ }
101
+ return extensions.map(extension => extension[extensionPlugin]);
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Shielding plugin source from Lexical's import
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /** A plugin node core recognized, and the stand-in that holds its place. */
109
+
110
+ const PRIVATE_USE_RANGES = [[0xe000, 0xf8ff], [0xf0000, 0xffffd], [0x100000, 0x10fffd]];
111
+
112
+ /** Private-use characters that do not occur in `text`, in order. */
113
+ export function* absentCharacters(text) {
114
+ const present = new Set(text);
115
+ for (const [first, last] of PRIVATE_USE_RANGES) {
116
+ for (let codePoint = first; codePoint <= last; codePoint++) {
117
+ const character = String.fromCodePoint(codePoint);
118
+ if (!present.has(character)) {
119
+ yield character;
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * The extension nodes core recognizes in `markdown`, in document order: inline
127
+ * nodes anywhere, and block nodes at the top level only (block syntax nested
128
+ * in a list item, quote, or table cell stays text in RichText).
129
+ */
130
+ function recognizedNodes(markdown, plugins) {
131
+ const adoptable = [];
132
+ let total = 0;
133
+ const visit = (node, isTopLevel) => {
134
+ if (node == null || typeof node !== 'object') {
135
+ return;
136
+ }
137
+ const candidate = node;
138
+ if (candidate.type === 'extension') {
139
+ total++;
140
+ const extension = node;
141
+ if (typeof extension.source === 'string' && (extension.display === 'inline' || isTopLevel)) {
142
+ adoptable.push(extension);
143
+ }
144
+ return;
145
+ }
146
+ if (Array.isArray(candidate.children)) {
147
+ for (const child of candidate.children) {
148
+ visit(child, false);
149
+ }
150
+ }
151
+ };
152
+ const root = parseMarkdownAst(markdown, {
153
+ plugins: [...plugins]
154
+ });
155
+ for (const block of root.children) {
156
+ visit(block, true);
157
+ }
158
+ return {
159
+ adoptable,
160
+ total
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Replaces the source of every plugin node core recognizes in `markdown` with
166
+ * a stand-in, so Lexical's transformers never read plugin source as base
167
+ * Markdown (spec:AST-064 FR5). A node whose source core does not recognize at
168
+ * any one occurrence — a span split across a container's line markers — stays
169
+ * text.
170
+ */
171
+ export function shieldExtensionSources(markdown, plugins) {
172
+ const standIns = new Map();
173
+ if (plugins.length === 0) {
174
+ return {
175
+ markdown,
176
+ standIns
177
+ };
178
+ }
179
+ const {
180
+ adoptable,
181
+ total
182
+ } = recognizedNodes(markdown, plugins);
183
+ if (adoptable.length === 0) {
184
+ return {
185
+ markdown,
186
+ standIns
187
+ };
188
+ }
189
+ const versions = new Map(plugins.map(plugin => [plugin.name, plugin.apiVersion]));
190
+ const tokens = absentCharacters(markdown);
191
+ // How many recognized nodes with each source are still to be placed.
192
+ const remaining = new Map();
193
+ for (const node of adoptable) {
194
+ const source = node.source ?? '';
195
+ remaining.set(source, (remaining.get(source) ?? 0) + 1);
196
+ }
197
+ let text = markdown;
198
+ let shielded = 0;
199
+ let searchFrom = 0;
200
+ for (const node of adoptable) {
201
+ const source = node.source ?? '';
202
+ const token = tokens.next().value;
203
+ if (token == null || source === '') {
204
+ break;
205
+ }
206
+ const first = text.indexOf(source, searchFrom);
207
+ // When every remaining occurrence of the source is a node core
208
+ // recognized, the first occurrence is this node: no need to ask core.
209
+ const isEveryOccurrence = occurrences(text, source, searchFrom) === (remaining.get(source) ?? 0);
210
+ remaining.set(source, (remaining.get(source) ?? 1) - 1);
211
+ for (let at = first; at !== -1; at = text.indexOf(source, at + 1)) {
212
+ const candidate = text.slice(0, at) + token + text.slice(at + source.length);
213
+ // The right occurrence is the one whose replacement removes exactly
214
+ // this node from core's parse and leaves every other node in place.
215
+ if (isEveryOccurrence || recognizedNodes(candidate, plugins).total === total - shielded - 1) {
216
+ text = candidate;
217
+ shielded++;
218
+ searchFrom = at + token.length;
219
+ standIns.set(token, {
220
+ node,
221
+ apiVersion: versions.get(node.plugin) ?? 1
222
+ });
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ return {
228
+ markdown: text,
229
+ standIns
230
+ };
231
+ }
232
+
233
+ /** How many times `source` occurs in `text` from `from`, overlaps included. */
234
+ function occurrences(text, source, from) {
235
+ let count = 0;
236
+ for (let at = text.indexOf(source, from); at !== -1; at = text.indexOf(source, at + 1)) {
237
+ count++;
238
+ }
239
+ return count;
240
+ }
241
+ function $nodeFor({
242
+ node,
243
+ apiVersion
244
+ }, format = 0) {
245
+ return $createRichTextExtensionNode({
246
+ plugin: node.plugin,
247
+ apiVersion,
248
+ name: node.name,
249
+ display: node.display,
250
+ data: node.data,
251
+ source: node.source ?? ''
252
+ }, format);
253
+ }
254
+
255
+ /**
256
+ * Puts a RichTextExtensionNode in place of every stand-in Lexical imported
257
+ * into `holder`: inline nodes where their stand-in is, and a block node in
258
+ * place of the paragraph that holds only its stand-in.
259
+ */
260
+ export function $restoreExtensionSources(holder, standIns) {
261
+ if (standIns.size === 0) {
262
+ return;
263
+ }
264
+ for (const textNode of holder.getAllTextNodes()) {
265
+ $restoreInText(textNode, holder, standIns);
266
+ }
267
+ }
268
+ function $restoreInText(textNode, holder, standIns) {
269
+ const text = textNode.getTextContent();
270
+ const offsets = new Set();
271
+ let offset = 0;
272
+ for (const character of text) {
273
+ if (standIns.has(character)) {
274
+ offsets.add(offset);
275
+ offsets.add(offset + character.length);
276
+ }
277
+ offset += character.length;
278
+ }
279
+ offsets.delete(0);
280
+ offsets.delete(text.length);
281
+ if (offsets.size === 0 && !standIns.has(text)) {
282
+ return;
283
+ }
284
+ const pieces = offsets.size === 0 ? [textNode] : textNode.splitText(...[...offsets].sort((a, b) => a - b));
285
+ for (const piece of pieces) {
286
+ const shielded = standIns.get(piece.getTextContent());
287
+ if (shielded == null) {
288
+ continue;
289
+ }
290
+ if (shielded.node.display === 'inline') {
291
+ // The node keeps the text formats its stand-in had: a node inside
292
+ // emphasis, strong, or strikethrough stays inside it.
293
+ piece.replace($nodeFor(shielded, piece.getFormat()));
294
+ continue;
295
+ }
296
+ const paragraph = piece.getParent();
297
+ if ($isParagraphNode(paragraph) && paragraph.getParent()?.is(holder) === true) {
298
+ $placeBlockNode(paragraph, piece, $nodeFor(shielded));
299
+ } else {
300
+ // A block stand-in that did not land in a top-level paragraph keeps
301
+ // the node's source as text.
302
+ piece.replace($createTextNode(shielded.node.source ?? ''));
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Puts a block node where its stand-in sits in `paragraph`, splitting the
309
+ * paragraph around it: lines before the stand-in stay a paragraph, the node
310
+ * follows as its own top-level block, and lines after it become a new
311
+ * paragraph — as core reads a block that starts on the line after a paragraph
312
+ * line.
313
+ */
314
+ function $placeBlockNode(paragraph, standIn, block) {
315
+ const after = standIn.getNextSiblings();
316
+ const before = standIn.getPreviousSiblings();
317
+ // The line breaks around the stand-in end and start the neighbouring lines.
318
+ if ($isLineBreakNode(before[before.length - 1])) {
319
+ before[before.length - 1]?.remove();
320
+ }
321
+ if ($isLineBreakNode(after[0])) {
322
+ after[0]?.remove();
323
+ after.shift();
324
+ }
325
+ standIn.remove();
326
+ if (after.length > 0) {
327
+ const rest = $createParagraphNode();
328
+ rest.append(...after);
329
+ paragraph.insertAfter(rest);
330
+ }
331
+ paragraph.insertAfter(block);
332
+ if (paragraph.getChildrenSize() === 0) {
333
+ paragraph.remove();
334
+ }
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Export
339
+ // ---------------------------------------------------------------------------
340
+
341
+ const NEVER = /(?!)/;
342
+
343
+ /**
344
+ * Exports every plugin node as its exact source (spec:AST-064 FR10). Always
345
+ * part of RichText's export, whatever transformers the caller passes, so no
346
+ * transformer set can drop a plugin node's source.
347
+ */
348
+ export const EXTENSION_EXPORT_TRANSFORMERS = [{
349
+ dependencies: [RichTextExtensionNode],
350
+ export: node => $isRichTextExtensionNode(node) && !node.isInline() ? node.getSource() : null,
351
+ regExp: NEVER,
352
+ replace: () => false,
353
+ type: 'element'
354
+ }, {
355
+ dependencies: [RichTextExtensionNode],
356
+ export: node => $isRichTextExtensionNode(node) && node.isInline() ? node.getSource() : null,
357
+ importRegExp: NEVER,
358
+ regExp: NEVER,
359
+ replace: () => {},
360
+ type: 'text-match'
361
+ }];
362
+
363
+ /**
364
+ * A text view of an inline plugin node for Lexical's exporter: it reads as
365
+ * text holding `placeholder`, with the node's formats, so emphasis, strong, or
366
+ * strikethrough around the node wraps it instead of closing before it. The
367
+ * caller writes the node's source in place of the placeholder.
368
+ */
369
+ export function extensionTextView(node, placeholder) {
370
+ const view = Object.create(TextNode.prototype);
371
+ // Reads of the view's format go to the node itself through its key.
372
+ Object.assign(view, {
373
+ __key: node.getKey()
374
+ });
375
+ view.getTextContent = () => placeholder;
376
+ view.getParent = () => node.getParent();
377
+ return view;
378
+ }
379
+ const withExport = new WeakMap();
380
+
381
+ /**
382
+ * `transformers` with the plugin node export first, the same array for the
383
+ * same transformers so callers that cache by transformer list keep hitting.
384
+ */
385
+ export function withExtensionExport(transformers) {
386
+ let combined = withExport.get(transformers);
387
+ if (combined == null) {
388
+ combined = [...EXTENSION_EXPORT_TRANSFORMERS, ...transformers];
389
+ withExport.set(transformers, combined);
390
+ }
391
+ return combined;
392
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file markdownHardLineBreak.ts
3
+ * @input Uses lexical (LineBreakNode) and @lexical/markdown
4
+ * (TextMatchTransformer).
5
+ * @output Exports HARD_LINE_BREAK, the transformer that writes a line break
6
+ * added in the editor (Shift+Enter) as a Markdown hard break, `\` and a
7
+ * line ending, and drops a line break that ends its block.
8
+ * @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts). Import joins
9
+ * soft line breaks into their block (markdownSource.ts), so every line
10
+ * break left in a paragraph, list item, or quote is a hard one. Lexical
11
+ * writes the breaks it imported as hard with their own marker (two spaces
12
+ * or a backslash); a break typed in the editor has none and would be
13
+ * written as a plain line ending, which reads back as a soft break and
14
+ * loses the line (spec:AST-062 FR3). Headings are one line in Markdown, so
15
+ * a break typed in one keeps Lexical's plain line ending. A hard break has
16
+ * no form at the end of a block: CommonMark reads its backslash as a
17
+ * literal character there, and shows nothing for two trailing spaces. So a
18
+ * line break that only other breaks or spaces follow is written as nothing.
19
+ */
20
+ import type { TextMatchTransformer } from '@lexical/markdown';
21
+ import { LineBreakNode } from 'lexical';
22
+ /** Whether Lexical's Markdown import marked this line break as a hard one. */
23
+ export declare function isMarkedHardLineBreak(node: LineBreakNode): boolean;
24
+ export declare const HARD_LINE_BREAK: TextMatchTransformer;
25
+ //# sourceMappingURL=markdownHardLineBreak.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownHardLineBreak.d.ts","sourceRoot":"","sources":["../src/markdownHardLineBreak.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAgC,aAAa,EAAC,MAAM,SAAS,CAAC;AAGrE,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAMlE;AAkBD,eAAO,MAAM,eAAe,EAAE,oBAmB7B,CAAC"}
@@ -0,0 +1,59 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownHardLineBreak.ts
5
+ * @input Uses lexical (LineBreakNode) and @lexical/markdown
6
+ * (TextMatchTransformer).
7
+ * @output Exports HARD_LINE_BREAK, the transformer that writes a line break
8
+ * added in the editor (Shift+Enter) as a Markdown hard break, `\` and a
9
+ * line ending, and drops a line break that ends its block.
10
+ * @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts). Import joins
11
+ * soft line breaks into their block (markdownSource.ts), so every line
12
+ * break left in a paragraph, list item, or quote is a hard one. Lexical
13
+ * writes the breaks it imported as hard with their own marker (two spaces
14
+ * or a backslash); a break typed in the editor has none and would be
15
+ * written as a plain line ending, which reads back as a soft break and
16
+ * loses the line (spec:AST-062 FR3). Headings are one line in Markdown, so
17
+ * a break typed in one keeps Lexical's plain line ending. A hard break has
18
+ * no form at the end of a block: CommonMark reads its backslash as a
19
+ * literal character there, and shows nothing for two trailing spaces. So a
20
+ * line break that only other breaks or spaces follow is written as nothing.
21
+ */
22
+
23
+ import { $isHeadingNode } from '@lexical/rich-text';
24
+ import { $findMatchingParent } from '@lexical/utils';
25
+ import { $isLineBreakNode, $isTextNode, LineBreakNode } from 'lexical';
26
+ /** Whether Lexical's Markdown import marked this line break as a hard one. */
27
+ export function isMarkedHardLineBreak(node) {
28
+ const serialized = node.exportJSON();
29
+ return typeof serialized.$?.mdHardLineBreak === 'string' && serialized.$.mdHardLineBreak !== '';
30
+ }
31
+
32
+ /** Whether only line breaks and blank text follow `node` in its block. */
33
+ function $endsItsBlock(node) {
34
+ for (let next = node.getNextSibling(); next != null; next = next.getNextSibling()) {
35
+ const isBlankText = $isTextNode(next) && next.getTextContent().trim() === '';
36
+ if (!$isLineBreakNode(next) && !isBlankText) {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ }
42
+ export const HARD_LINE_BREAK = {
43
+ dependencies: [LineBreakNode],
44
+ export: node => {
45
+ if (!$isLineBreakNode(node) ||
46
+ // A heading is one line in Markdown; a break inside one has no
47
+ // hard-break form, so it keeps Lexical's plain line ending.
48
+ $findMatchingParent(node, $isHeadingNode) != null) {
49
+ return null;
50
+ }
51
+ if ($endsItsBlock(node)) {
52
+ return '';
53
+ }
54
+ return isMarkedHardLineBreak(node) ? null : '\\\n';
55
+ },
56
+ // Export only: Lexical's own import recognizes hard breaks.
57
+ regExp: /(?!)/,
58
+ type: 'text-match'
59
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file markdownHeading.ts
3
+ * @input Uses @lexical/markdown (Lexical's HEADING element transformer).
4
+ * @output Exports HEADING_MARKERS, Lexical's heading transformer reading an
5
+ * ATX heading marker as CommonMark does.
6
+ * @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts) in place of
7
+ * Lexical's HEADING, whose pattern allows no indentation. Matches core
8
+ * Markdown's ATX headings (CommonMark 0.31 §4.2). Typing shortcuts keep
9
+ * Lexical's own trigger: `# ` makes a heading. Text that starts like a
10
+ * marker is escaped on export by markdownSource.ts.
11
+ */
12
+ import { type ElementTransformer } from '@lexical/markdown';
13
+ /**
14
+ * An ATX heading marker: up to three spaces of indentation, then one to six
15
+ * `#` and a space, so ` # h` imports as a heading, as in core Markdown. A
16
+ * typed line becomes a heading only at Lexical's own `# `, so typing
17
+ * ` # ` changes nothing new.
18
+ */
19
+ export declare const HEADING_MARKERS: ElementTransformer;
20
+ //# sourceMappingURL=markdownHeading.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownHeading.d.ts","sourceRoot":"","sources":["../src/markdownHeading.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,OAAO,EAAU,KAAK,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAKnE;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,kBAO7B,CAAC"}
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownHeading.ts
5
+ * @input Uses @lexical/markdown (Lexical's HEADING element transformer).
6
+ * @output Exports HEADING_MARKERS, Lexical's heading transformer reading an
7
+ * ATX heading marker as CommonMark does.
8
+ * @position Part of DEFAULT_TRANSFORMERS (markdownTable.ts) in place of
9
+ * Lexical's HEADING, whose pattern allows no indentation. Matches core
10
+ * Markdown's ATX headings (CommonMark 0.31 §4.2). Typing shortcuts keep
11
+ * Lexical's own trigger: `# ` makes a heading. Text that starts like a
12
+ * marker is escaped on export by markdownSource.ts.
13
+ */
14
+
15
+ import { HEADING } from '@lexical/markdown';
16
+
17
+ /** Lexical's own heading marker, which the typing shortcut keeps using. */
18
+ const SHORTCUT_MARKER = /^(#{1,6})\s/;
19
+
20
+ /**
21
+ * An ATX heading marker: up to three spaces of indentation, then one to six
22
+ * `#` and a space, so ` # h` imports as a heading, as in core Markdown. A
23
+ * typed line becomes a heading only at Lexical's own `# `, so typing
24
+ * ` # ` changes nothing new.
25
+ */
26
+ export const HEADING_MARKERS = {
27
+ ...HEADING,
28
+ regExp: /^ {0,3}(#{1,6})\s/,
29
+ replace: (parentNode, children, match, isImport) => isImport || SHORTCUT_MARKER.test(match[0]) ? HEADING.replace(parentNode, children, match, isImport) : false
30
+ };
@@ -0,0 +1,5 @@
1
+ import { type TextMatchTransformer } from '@lexical/markdown';
2
+ /** `url` as a Markdown destination that reads back as `url`. */
3
+ export declare function markdownDestination(url: string): string;
4
+ export declare const LINK_KEEPING_DESTINATIONS: TextMatchTransformer;
5
+ //# sourceMappingURL=markdownLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownLink.d.ts","sourceRoot":"","sources":["../src/markdownLink.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAO,KAAK,oBAAoB,EAAC,MAAM,mBAAmB,CAAC;AA6BlE,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUvD;AAED,eAAO,MAAM,yBAAyB,EAAE,oBAoBvC,CAAC"}
@@ -0,0 +1,80 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownLink.ts
5
+ * @input Uses @lexical/link and @lexical/markdown (LINK).
6
+ * @output Exports LINK_KEEPING_DESTINATIONS, Lexical's link transformer with
7
+ * one change on export. Import is Lexical's own, which reads every
8
+ * CommonMark destination and title shape, an angle-bracket destination
9
+ * (`<https://e.com/a b>`) included, and decodes backslash escapes and
10
+ * character references in both. A destination that needs no escape is
11
+ * written by Lexical's export too, which escapes parentheses and an `&`
12
+ * that starts a character reference (`https://e.com/a_(b)` as
13
+ * `https://e.com/a_\(b\)`). One that holds whitespace or opens with `<`
14
+ * is written here in angle brackets, and one with a backslash or
15
+ * unbalanced parentheses with those escaped; both escape a
16
+ * reference-starting `&` as Lexical does, so every link reads back as
17
+ * the same link, whether it was imported or made in the editor
18
+ * (spec:AST-062 FR3).
19
+ * @position Replaces Lexical's LINK in DEFAULT_TRANSFORMERS and in table
20
+ * cells (markdownTable.ts).
21
+ */
22
+
23
+ import { $isAutoLinkNode, $isLinkNode } from '@lexical/link';
24
+ import { LINK } from '@lexical/markdown';
25
+
26
+ /** Whether every `)` in `url` closes an earlier `(` and every `(` closes. */
27
+ function isBalanced(url) {
28
+ let depth = 0;
29
+ for (const character of url) {
30
+ if (character === '(') {
31
+ depth++;
32
+ } else if (character === ')') {
33
+ depth--;
34
+ if (depth < 0) {
35
+ return false;
36
+ }
37
+ }
38
+ }
39
+ return depth === 0;
40
+ }
41
+
42
+ /**
43
+ * `value` with every `&` that would start a character reference written as
44
+ * `&#38;`, since import decodes references in a destination and a title.
45
+ */
46
+ function escapeCharacterReferences(value) {
47
+ return value.replace(/&(?=#\d+;|#[Xx][\dA-Fa-f]+;|[A-Za-z][\dA-Za-z]*;)/g, '&#38;');
48
+ }
49
+
50
+ /** `url` as a Markdown destination that reads back as `url`. */
51
+ export function markdownDestination(url) {
52
+ if (/\s/.test(url) || url.startsWith('<')) {
53
+ // Only an angle-bracket destination may hold a space or open with `<`
54
+ // (CommonMark §6.3).
55
+ return `<${url.replace(/[\\<>]/g, '\\$&')}>`;
56
+ }
57
+ // A backslash before punctuation would read back as an escape, so every
58
+ // backslash is written escaped; so is every parenthesis that does not
59
+ // balance.
60
+ return url.replace(isBalanced(url) ? /\\/g : /[\\()]/g, '\\$&');
61
+ }
62
+ export const LINK_KEEPING_DESTINATIONS = {
63
+ // Lexical's own import reads every CommonMark destination and title shape,
64
+ // an angle-bracket destination included.
65
+ ...LINK,
66
+ export: (node, exportChildren, exportFormat) => {
67
+ if (!$isLinkNode(node) || $isAutoLinkNode(node)) {
68
+ return null;
69
+ }
70
+ const url = node.getURL();
71
+ const destination = markdownDestination(url);
72
+ if (destination === url) {
73
+ return LINK.export?.(node, exportChildren, exportFormat) ?? null;
74
+ }
75
+ const text = exportChildren(node);
76
+ const written = escapeCharacterReferences(destination);
77
+ const title = node.getTitle();
78
+ return title == null || title === '' ? `[${text}](${written})` : `[${text}](${written} "${escapeCharacterReferences(title).replace(/([\\"])/g, '\\$1')}")`;
79
+ }
80
+ };
@@ -0,0 +1,3 @@
1
+ import type { ElementTransformer } from '@lexical/markdown';
2
+ export declare const LIST_EXPORT: ElementTransformer;
3
+ //# sourceMappingURL=markdownListExport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownListExport.d.ts","sourceRoot":"","sources":["../src/markdownListExport.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAoD1D,eAAO,MAAM,WAAW,EAAE,kBAQzB,CAAC"}