@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,1127 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file markdownCharacterReferences.ts
5
+ * @input Uses lexical, @lexical/link, @lexical/code, and core Markdown's
6
+ * decodeMarkdownCharacterReferences.
7
+ * @output Exports protectRefusedLinks, protectBackslashEscapes,
8
+ * protectLinkDestinationParentheses, and protectCharacterReferences, which
9
+ * swap every link core refuses, every backslash escape, every parenthesis
10
+ * inside a link destination, and every character reference in Markdown
11
+ * source for a private-use stand-in before Lexical imports it, and
12
+ * $restoreCharacterReferences, which puts the literal or decoded text in
13
+ * place of the stand-ins afterwards.
14
+ * @position Used by importMarkdownKeepingSource (markdownSource.ts), so every
15
+ * block — paragraphs, headings, lists, quotes, table cells, link text and
16
+ * destinations — decodes references with the one decoder core Markdown
17
+ * renders with (spec:AST-061 FR7, DEC-5), in one pass over the source and
18
+ * one over the imported text. The stand-ins mean nothing to Lexical's
19
+ * Markdown import, so a decoded `*` never starts emphasis, and no `&#digits;`
20
+ * reaches Lexical's own decoding, which ignores escapes and throws on a
21
+ * number past Unicode. Code keeps references as written.
22
+ * Backslash escapes go through the same way, so an escaped character is
23
+ * literal text wherever it is — `\[x](y)` is text, not a link, as in core
24
+ * Markdown (spec:AST-061 FR7, spec:AST-062 FR3) — and code keeps its
25
+ * backslashes. Parentheses inside a link destination go through the same
26
+ * way, so Lexical, which reads none in a destination, reads balanced ones
27
+ * as core does.
28
+ */
29
+
30
+ import { $isCodeNode } from '@lexical/code';
31
+ import { $isLinkNode } from '@lexical/link';
32
+ import { $isTableCellNode } from '@lexical/table';
33
+ import { $dfs, $findMatchingParent } from '@lexical/utils';
34
+ import { $getState, $isElementNode, $isTextNode, $setState, createState } from 'lexical';
35
+ import { decodeMarkdownCharacterReferences, parseInlineAst } from '@blue-patterns/core/Markdown/parser';
36
+
37
+ /**
38
+ * What a stand-in replaced: a reference as written, an escaped `&`, or a
39
+ * backslash-escaped character.
40
+ */
41
+
42
+ // Something that looks like a reference; the decoder decides if it is one.
43
+ const REFERENCE_LIKE = /&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[A-Za-z][A-Za-z0-9]*);/y;
44
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
45
+ const PRIVATE_USE_RANGES = [[0xe000, 0xf8ff], [0xf0000, 0xffffd], [0x100000, 0x10fffd]];
46
+
47
+ /** Private-use characters that do not occur in `text`, in order. */
48
+ export function* absentCharacters(text) {
49
+ const present = new Set(text);
50
+ for (const [first, last] of PRIVATE_USE_RANGES) {
51
+ for (let codePoint = first; codePoint <= last; codePoint++) {
52
+ const character = String.fromCodePoint(codePoint);
53
+ if (!present.has(character)) {
54
+ yield character;
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The ranges of `text` that are code: fenced code blocks, and code spans,
62
+ * where a backtick run opens a span only if a later run of the same length
63
+ * closes it (CommonMark 0.31). One scan collects the runs; runs of each length
64
+ * are searched by position, so the whole is O(n log n).
65
+ */
66
+ function codeRanges(text) {
67
+ const {
68
+ fenced,
69
+ spans
70
+ } = codeRangesByKind(text);
71
+ return [...fenced, ...spans].sort((a, b) => a[0] - b[0]);
72
+ }
73
+
74
+ /** The fenced code blocks of `text`, and its code spans, each in order. */
75
+ function codeRangesByKind(text) {
76
+ const ranges = [];
77
+ // Fenced code blocks, line by line.
78
+ let lineStart = 0;
79
+ let fence = null;
80
+ while (lineStart <= text.length) {
81
+ const lineEnd = text.indexOf('\n', lineStart);
82
+ const end = lineEnd === -1 ? text.length : lineEnd;
83
+ const line = text.slice(lineStart, end);
84
+ if (fence == null) {
85
+ const open = FENCE_OPEN.exec(line);
86
+ if (open != null) {
87
+ fence = {
88
+ marker: open[1],
89
+ start: lineStart
90
+ };
91
+ }
92
+ } else if (new RegExp(`^ {0,3}\\${fence.marker[0]}{${fence.marker.length},}[ \\t]*$`).test(line)) {
93
+ ranges.push([fence.start, end]);
94
+ fence = null;
95
+ }
96
+ if (lineEnd === -1) {
97
+ break;
98
+ }
99
+ lineStart = lineEnd + 1;
100
+ }
101
+ if (fence != null) {
102
+ ranges.push([fence.start, text.length]);
103
+ }
104
+ const fencedCount = ranges.length;
105
+ // Code spans, outside fenced code. A backtick run is a string of backticks
106
+ // with no backtick on either side. Backslashes are literal inside a code
107
+ // span, so they never hide a closing run; an odd number of backslashes
108
+ // before a run only makes its first backtick literal, so the rest of the
109
+ // run (if any) may still open a span.
110
+ const runs = [];
111
+ let index = 0;
112
+ let fenceIndex = 0;
113
+ while (index < text.length) {
114
+ const current = ranges[fenceIndex];
115
+ if (current != null && index >= current[0]) {
116
+ index = current[1];
117
+ fenceIndex++;
118
+ continue;
119
+ }
120
+ if (text[index] === '`') {
121
+ let end = index;
122
+ while (text[end] === '`') {
123
+ end++;
124
+ }
125
+ runs.push({
126
+ start: index,
127
+ length: end - index
128
+ });
129
+ index = end;
130
+ continue;
131
+ }
132
+ index++;
133
+ }
134
+ // Each run's position in the list of runs of its length, for finding the
135
+ // next run of a given length after a point by binary search.
136
+ const byLength = new Map();
137
+ for (const run of runs) {
138
+ const starts = byLength.get(run.length) ?? [];
139
+ starts.push(run.start);
140
+ byLength.set(run.length, starts);
141
+ }
142
+ const nextRunAfter = (length, after) => {
143
+ const starts = byLength.get(length);
144
+ if (starts == null) {
145
+ return null;
146
+ }
147
+ let low = 0;
148
+ let high = starts.length;
149
+ while (low < high) {
150
+ const middle = low + high >> 1;
151
+ if (starts[middle] < after) {
152
+ low = middle + 1;
153
+ } else {
154
+ high = middle;
155
+ }
156
+ }
157
+ return starts[low] ?? null;
158
+ };
159
+ let resumeAt = 0;
160
+ for (const run of runs) {
161
+ if (run.start < resumeAt) {
162
+ continue;
163
+ }
164
+ let backslashes = 0;
165
+ while (text[run.start - 1 - backslashes] === '\\') {
166
+ backslashes++;
167
+ }
168
+ const isEscaped = backslashes % 2 === 1;
169
+ const openStart = isEscaped ? run.start + 1 : run.start;
170
+ const openLength = isEscaped ? run.length - 1 : run.length;
171
+ if (openLength === 0) {
172
+ continue;
173
+ }
174
+ const closer = nextRunAfter(openLength, run.start + run.length);
175
+ if (closer == null) {
176
+ continue;
177
+ }
178
+ ranges.push([openStart, closer + openLength]);
179
+ resumeAt = closer + openLength;
180
+ }
181
+ return {
182
+ fenced: ranges.slice(0, fencedCount),
183
+ spans: ranges.slice(fencedCount).sort((a, b) => a[0] - b[0])
184
+ };
185
+ }
186
+
187
+ /** The characters a backslash escapes (CommonMark 0.31 §2.4). */
188
+ const ESCAPABLE = new Set('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~');
189
+
190
+ /**
191
+ * Returns `markdown` with every backslash escape outside code — a backslash
192
+ * before ASCII punctuation — replaced by a private-use stand-in for the
193
+ * escaped character, and every backslash in code by a stand-in for itself,
194
+ * which Lexical would otherwise drop from a code span — except before `|`,
195
+ * which a table reads before its code. A backslash before anything else
196
+ * outside code stays as written. One pass over the source.
197
+ */
198
+ export function protectBackslashEscapes(markdown) {
199
+ const standIns = new Map();
200
+ if (!markdown.includes('\\')) {
201
+ return {
202
+ markdown,
203
+ standIns
204
+ };
205
+ }
206
+ const available = absentCharacters(markdown);
207
+ const byKey = new Map();
208
+ const standInFor = (key, value) => {
209
+ const existing = byKey.get(key);
210
+ if (existing != null) {
211
+ return existing;
212
+ }
213
+ const next = available.next();
214
+ if (next.done === true) {
215
+ // Every private-use character is in the text: leave the rest as
216
+ // written.
217
+ return null;
218
+ }
219
+ byKey.set(key, next.value);
220
+ standIns.set(next.value, value);
221
+ return next.value;
222
+ };
223
+ const code = codeRanges(markdown);
224
+ let output = '';
225
+ let copied = 0;
226
+ let codeIndex = 0;
227
+ let index = markdown.indexOf('\\');
228
+ while (index !== -1) {
229
+ while (code[codeIndex] != null && code[codeIndex][1] <= index) {
230
+ codeIndex++;
231
+ }
232
+ const range = code[codeIndex];
233
+ const character = markdown[index + 1];
234
+ const inCode = range != null && range[0] <= index;
235
+ // A table splits its cells before it reads code, so `\|` in code is the
236
+ // table's to read.
237
+ if (inCode && character === '|') {
238
+ index = markdown.indexOf('\\', index + 2);
239
+ continue;
240
+ }
241
+ const isEscape = !inCode && character != null && ESCAPABLE.has(character);
242
+ if (inCode || isEscape) {
243
+ const standIn = inCode ? standInFor('code', {
244
+ kind: 'backslash'
245
+ }) : standInFor(`escape:${character}`, {
246
+ kind: 'escape',
247
+ character: character ?? ''
248
+ });
249
+ if (standIn == null) {
250
+ break;
251
+ }
252
+ output += markdown.slice(copied, index) + standIn;
253
+ copied = index + (inCode ? 1 : 2);
254
+ index = markdown.indexOf('\\', copied);
255
+ continue;
256
+ }
257
+ index = markdown.indexOf('\\', index + 1);
258
+ }
259
+ return {
260
+ markdown: output + markdown.slice(copied),
261
+ standIns
262
+ };
263
+ }
264
+
265
+ /**
266
+ * For each `[`, `]`, `(`, and `)` in `text` outside code and not escaped,
267
+ * the index of the bracket or parenthesis it pairs with by nesting; -1 for
268
+ * the rest. One pass, so a candidate never rescans the text.
269
+ */
270
+ function pairedDelimiters(text, code) {
271
+ const pair = new Int32Array(text.length).fill(-1);
272
+ const brackets = [];
273
+ const parentheses = [];
274
+ let codeIndex = 0;
275
+ for (let index = 0; index < text.length; index++) {
276
+ while (codeIndex < code.length && code[codeIndex][1] <= index) {
277
+ codeIndex++;
278
+ }
279
+ if (codeIndex < code.length && code[codeIndex][0] <= index) {
280
+ index = code[codeIndex][1] - 1;
281
+ continue;
282
+ }
283
+ const character = text[index];
284
+ const open = character === ']' ? brackets.pop() : character === ')' ? parentheses.pop() : undefined;
285
+ if (character === '\\') {
286
+ index++;
287
+ } else if (character === '[') {
288
+ brackets.push(index);
289
+ } else if (character === '(') {
290
+ parentheses.push(index);
291
+ } else if (open !== undefined) {
292
+ pair[index] = open;
293
+ pair[open] = index;
294
+ }
295
+ }
296
+ return pair;
297
+ }
298
+
299
+ /**
300
+ * Where a destination that opens with `<` ends — the index of the `)` that
301
+ * closes the link — read as core Markdown reads it (CommonMark 0.31 §6.3):
302
+ * `'refused'` when it opens with `<` but is no angle-bracket destination,
303
+ * and null when the content after `open` (the `(`) does not open with `<`.
304
+ * Inside the brackets parentheses are plain characters; a line ending, even
305
+ * escaped, or an unescaped `<` makes it no destination; after the `>` come
306
+ * only spaces, an optional title, and the `)`.
307
+ */
308
+ function angleDestinationEnd(text, open) {
309
+ let index = open + 1;
310
+ while (text[index] === ' ' || text[index] === '\t') {
311
+ index++;
312
+ }
313
+ if (text[index] !== '<') {
314
+ return null;
315
+ }
316
+ for (index++; index < text.length; index++) {
317
+ const character = text[index];
318
+ if (character === '\\' && text[index + 1] !== '\n' && text[index + 1] !== '\r') {
319
+ index++;
320
+ } else if (character === '\n' || character === '\r' || character === '<') {
321
+ return 'refused';
322
+ } else if (character === '>') {
323
+ break;
324
+ }
325
+ }
326
+ if (index >= text.length) {
327
+ return 'refused';
328
+ }
329
+ index++;
330
+ while (/\s/.test(text[index] ?? '')) {
331
+ index++;
332
+ }
333
+ const quote = text[index];
334
+ if (quote === '"' || quote === "'" || quote === '(') {
335
+ const closer = quote === '(' ? ')' : quote;
336
+ for (index++; index < text.length && text[index] !== closer; index++) {
337
+ if (text[index] === '\\') {
338
+ index++;
339
+ } else if (quote === '(' && text[index] === '(') {
340
+ return 'refused';
341
+ }
342
+ }
343
+ index++;
344
+ while (/\s/.test(text[index] ?? '')) {
345
+ index++;
346
+ }
347
+ }
348
+ return text[index] === ')' ? index : 'refused';
349
+ }
350
+
351
+ /**
352
+ * Whether core Markdown refuses a link to `destination`, as written in the
353
+ * source: its own parser decides, so both surfaces refuse the same
354
+ * destinations by one policy.
355
+ */
356
+ function coreRefusesDestination(destination) {
357
+ const probe = `[x](${destination})`;
358
+ const [only, ...rest] = parseInlineAst(probe);
359
+ return rest.length === 0 && only?.type === 'text' && only.value === probe;
360
+ }
361
+
362
+ /**
363
+ * Whether core Markdown refuses a link to `url`, a destination already
364
+ * decoded: the probe writes it back with every special character escaped.
365
+ * A URL that cannot be written back is refused.
366
+ */
367
+ export function coreRefusesUrl(url) {
368
+ if (/[\n\r]/.test(url)) {
369
+ return true;
370
+ }
371
+ // Angle brackets never decide a scheme, so the probe percent-encodes them:
372
+ // the URL then fits one angle-bracket destination even with spaces in it.
373
+ const escaped = url.replace(/[<>]/g, character => character === '<' ? '%3C' : '%3E').replace(/[\\()&[\]"']/g, character => `\\${character}`);
374
+ return coreRefusesDestination(/\s/.test(url) ? `<${escaped}>` : escaped);
375
+ }
376
+
377
+ /**
378
+ * Returns `markdown` with each inline link whose destination core Markdown
379
+ * refuses — `javascript:` and the like — kept from becoming a link
380
+ * (spec:AST-061 FR7). Where core shows the link as its source text, exactly
381
+ * as written, the whole source becomes one stand-in that comes back as that
382
+ * text. Where core reads no link there at all (its link text holds brackets
383
+ * of its own), only the `(` that opens the destination becomes a stand-in,
384
+ * so the text around it reads as core reads it, marks included. Brackets and
385
+ * parentheses pair by nesting, so link text holding brackets is found whole.
386
+ * Linear in the source: pairs come from one pass, and a destination inside a
387
+ * found link is not probed again. Run first, on the source as written;
388
+ * `$unwrapRefusedLinks` catches any link the editor still makes.
389
+ */
390
+ export function protectRefusedLinks(markdown) {
391
+ const standIns = new Map();
392
+ if (!markdown.includes('](')) {
393
+ return {
394
+ markdown,
395
+ standIns
396
+ };
397
+ }
398
+ const pair = pairedDelimiters(markdown, codeRanges(markdown));
399
+ // Ranges to replace, in order: a whole link, or the `(` that opens its
400
+ // destination.
401
+ const refused = [];
402
+ // Where the last probed destination ends: a link cannot sit inside one.
403
+ let probedTo = -1;
404
+ for (let index = markdown.indexOf(']('); index !== -1; index = markdown.indexOf('](', index + 1)) {
405
+ const open = pair[index];
406
+ // A destination in angle brackets ends where core ends it — parentheses
407
+ // inside the brackets are plain characters — so `[a](<b)c>)` is probed
408
+ // whole, not as `<b`.
409
+ const angleClose = angleDestinationEnd(markdown, index + 1);
410
+ const close = typeof angleClose === 'number' ? angleClose : pair[index + 1];
411
+ if (index < probedTo || open === -1 || close === -1) {
412
+ continue;
413
+ }
414
+ probedTo = close;
415
+ if (!coreRefusesDestination(markdown.slice(index + 2, close))) {
416
+ continue;
417
+ }
418
+ const source = markdown.slice(open, close + 1);
419
+ const [only, ...rest] = parseInlineAst(source);
420
+ if (rest.length === 0 && only?.type === 'text' && only.value === source) {
421
+ // A whole link stands for the refused links found inside it.
422
+ while (refused.length > 0 && refused[refused.length - 1].start >= open) {
423
+ refused.pop();
424
+ }
425
+ refused.push({
426
+ start: open,
427
+ end: close + 1,
428
+ whole: true
429
+ });
430
+ } else {
431
+ refused.push({
432
+ start: index + 1,
433
+ end: index + 2,
434
+ whole: false
435
+ });
436
+ }
437
+ }
438
+ if (refused.length === 0) {
439
+ return {
440
+ markdown,
441
+ standIns
442
+ };
443
+ }
444
+ const available = absentCharacters(markdown);
445
+ let output = '';
446
+ let copied = 0;
447
+ for (const {
448
+ start,
449
+ end,
450
+ whole
451
+ } of refused) {
452
+ const next = available.next();
453
+ if (next.done === true || start < copied) {
454
+ break;
455
+ }
456
+ standIns.set(next.value, whole ? {
457
+ kind: 'literal',
458
+ text: markdown.slice(start, end)
459
+ } : {
460
+ kind: 'escape',
461
+ character: '('
462
+ });
463
+ output += markdown.slice(copied, start) + next.value;
464
+ copied = end;
465
+ }
466
+ return {
467
+ markdown: output + markdown.slice(copied),
468
+ standIns
469
+ };
470
+ }
471
+
472
+ /**
473
+ * Marks text that holds one image's Markdown source. Export writes it
474
+ * unescaped, so it stays an image.
475
+ */
476
+ const imageSourceState = createState('astryxMdImage', {
477
+ parse: value => value === true
478
+ });
479
+
480
+ /** Whether `node` is text that holds an image's Markdown source. */
481
+ export function $isImageSource(node) {
482
+ return $isTextNode(node) && $getState(node, imageSourceState);
483
+ }
484
+
485
+ /**
486
+ * Returns `markdown` with every image outside code replaced by a private-use
487
+ * stand-in, and the source each stand-in replaced. An image starts with a `!`
488
+ * that is not escaped and the paired brackets of its text. An inline image
489
+ * (`![a](b)`) ends with the destination and title that close it, read whole
490
+ * by core's inline parser. A reference image (`![a][b]`, `![a][]`, `![a]`)
491
+ * ends with its label; its definition can sit in another block, so it is kept
492
+ * whether or not one exists. A link around an image keeps its own brackets,
493
+ * so the image stays inside the link's text.
494
+ */
495
+ export function protectImages(markdown) {
496
+ const images = new Map();
497
+ if (!markdown.includes('![')) {
498
+ return {
499
+ markdown,
500
+ images
501
+ };
502
+ }
503
+ const pair = pairedDelimiters(markdown, codeRanges(markdown));
504
+ const found = [];
505
+ for (let start = markdown.indexOf('!['); start !== -1; start = markdown.indexOf('![', start + 1)) {
506
+ const open = start + 1;
507
+ const textClose = pair[open];
508
+ if (textClose === -1 || textClose < open) {
509
+ continue;
510
+ }
511
+ // An image inside another image's text belongs to the outer one.
512
+ if (found.length > 0 && start < found[found.length - 1].end) {
513
+ continue;
514
+ }
515
+ let backslashes = 0;
516
+ while (markdown[start - 1 - backslashes] === '\\') {
517
+ backslashes++;
518
+ }
519
+ if (backslashes % 2 === 1) {
520
+ continue;
521
+ }
522
+ const after = markdown[textClose + 1];
523
+ if (after === '(') {
524
+ const angleClose = angleDestinationEnd(markdown, textClose + 1);
525
+ const close = typeof angleClose === 'number' ? angleClose : pair[textClose + 1];
526
+ if (close === -1 || angleClose === 'refused') {
527
+ continue;
528
+ }
529
+ const [only, ...rest] = parseInlineAst(markdown.slice(start, close + 1));
530
+ if (rest.length > 0 || only?.type !== 'image') {
531
+ continue;
532
+ }
533
+ found.push({
534
+ start,
535
+ end: close + 1
536
+ });
537
+ } else if (after === '[' && pair[textClose + 1] > textClose + 1) {
538
+ found.push({
539
+ start,
540
+ end: pair[textClose + 1] + 1
541
+ });
542
+ } else {
543
+ found.push({
544
+ start,
545
+ end: textClose + 1
546
+ });
547
+ }
548
+ }
549
+ if (found.length === 0) {
550
+ return {
551
+ markdown,
552
+ images
553
+ };
554
+ }
555
+ const available = absentCharacters(markdown);
556
+ let output = '';
557
+ let copied = 0;
558
+ for (const {
559
+ start,
560
+ end
561
+ } of found) {
562
+ const next = available.next();
563
+ if (next.done === true) {
564
+ break;
565
+ }
566
+ images.set(next.value, markdown.slice(start, end));
567
+ output += markdown.slice(copied, start) + next.value;
568
+ copied = end;
569
+ }
570
+ return {
571
+ markdown: output + markdown.slice(copied),
572
+ images
573
+ };
574
+ }
575
+
576
+ /**
577
+ * Puts each image's source back where its stand-in sits under `node`, as
578
+ * text marked as an image's source. The text is a token: a person deletes it
579
+ * whole, and autolinking leaves its URL alone. ImageSourceCaretPlugin keeps
580
+ * the caret out of it, so typing never replaces it.
581
+ */
582
+ export function $restoreImages(node, images) {
583
+ if (images.size === 0) {
584
+ return;
585
+ }
586
+ for (const text of node.getAllTextNodes()) {
587
+ const content = text.getTextContent();
588
+ const offsets = [];
589
+ let offset = 0;
590
+ for (const character of content) {
591
+ if (images.has(character)) {
592
+ offsets.push(offset, offset + character.length);
593
+ }
594
+ offset += character.length;
595
+ }
596
+ if (offsets.length === 0) {
597
+ continue;
598
+ }
599
+ const pieces = text.splitText(...offsets.filter(point => point > 0 && point < content.length));
600
+ for (const piece of pieces) {
601
+ const source = images.get(piece.getTextContent());
602
+ if (source == null) {
603
+ continue;
604
+ }
605
+ piece.setTextContent(source);
606
+ piece.setMode('token');
607
+ $setState(piece, imageSourceState, true);
608
+ }
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Takes every link under `node` whose URL core Markdown refuses out of its
614
+ * link, leaving its text in place, so no form of link text or destination
615
+ * the source pass did not find can store or draw an unsafe URL.
616
+ */
617
+ export function $unwrapRefusedLinks(node) {
618
+ for (const {
619
+ node: descendant
620
+ } of $dfs(node)) {
621
+ if ($isLinkNode(descendant) && coreRefusesUrl(descendant.getURL())) {
622
+ for (const child of descendant.getChildren()) {
623
+ descendant.insertBefore(child);
624
+ }
625
+ descendant.remove();
626
+ }
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Returns `markdown` with every parenthesis inside an inline link
632
+ * destination — between the destination's own parentheses, balanced, as core
633
+ * Markdown and CommonMark read them — replaced by a private-use stand-in for
634
+ * itself, outside code. Run after protectBackslashEscapes, so an escaped
635
+ * parenthesis is already a stand-in and never counts toward the balance.
636
+ */
637
+ export function protectLinkDestinationParentheses(markdown) {
638
+ const standIns = new Map();
639
+ if (!markdown.includes('](')) {
640
+ return {
641
+ markdown,
642
+ standIns
643
+ };
644
+ }
645
+ const available = absentCharacters(markdown);
646
+ const byCharacter = new Map();
647
+ const standInFor = character => {
648
+ const existing = byCharacter.get(character);
649
+ if (existing != null) {
650
+ return existing;
651
+ }
652
+ const next = available.next();
653
+ if (next.done === true) {
654
+ return null;
655
+ }
656
+ byCharacter.set(character, next.value);
657
+ standIns.set(next.value, {
658
+ kind: 'escape',
659
+ character
660
+ });
661
+ return next.value;
662
+ };
663
+ const code = codeRanges(markdown);
664
+ const inner = [];
665
+ let codeIndex = 0;
666
+ let index = markdown.indexOf('](');
667
+ while (index !== -1) {
668
+ while (code[codeIndex] != null && code[codeIndex][1] <= index) {
669
+ codeIndex++;
670
+ }
671
+ const range = code[codeIndex];
672
+ if (range != null && range[0] <= index) {
673
+ index = markdown.indexOf('](', range[1]);
674
+ continue;
675
+ }
676
+ // The destination's own parentheses are depth one; any inside it are
677
+ // deeper. A space at depth one ends the destination (a title follows);
678
+ // a space deeper, or no closing parenthesis, means no link.
679
+ const parentheses = [];
680
+ let depth = 1;
681
+ let position = index + 2;
682
+ let isLink = markdown[position] !== '<';
683
+ for (; isLink && position < markdown.length; position++) {
684
+ const character = markdown[position];
685
+ if (/\s/.test(character)) {
686
+ isLink = depth === 1;
687
+ break;
688
+ }
689
+ if (character === '(') {
690
+ depth++;
691
+ parentheses.push(position);
692
+ } else if (character === ')') {
693
+ depth--;
694
+ if (depth === 0) {
695
+ break;
696
+ }
697
+ parentheses.push(position);
698
+ }
699
+ }
700
+ if (isLink && depth <= 1) {
701
+ inner.push(...parentheses);
702
+ }
703
+ index = markdown.indexOf('](', position + 1);
704
+ }
705
+ if (inner.length === 0) {
706
+ return {
707
+ markdown,
708
+ standIns
709
+ };
710
+ }
711
+ let output = '';
712
+ let copied = 0;
713
+ for (const position of inner) {
714
+ const standIn = standInFor(markdown[position]);
715
+ if (standIn == null) {
716
+ break;
717
+ }
718
+ output += markdown.slice(copied, position) + standIn;
719
+ copied = position + 1;
720
+ }
721
+ return {
722
+ markdown: output + markdown.slice(copied),
723
+ standIns
724
+ };
725
+ }
726
+
727
+ /** The code spans a stand-in pass replaced: each stand-in's code. */
728
+
729
+ /** Whether `text` holds a `|` with no backslash before it. */
730
+ function hasUnescapedPipe(text) {
731
+ for (let index = text.indexOf('|'); index !== -1;) {
732
+ let backslashes = 0;
733
+ while (text[index - 1 - backslashes] === '\\') {
734
+ backslashes++;
735
+ }
736
+ if (backslashes % 2 === 0) {
737
+ return true;
738
+ }
739
+ index = text.indexOf('|', index + 1);
740
+ }
741
+ return false;
742
+ }
743
+
744
+ /**
745
+ * Test each cell on its own. One regex over the whole row backtracks in
746
+ * quadratic time on a long run of spaces.
747
+ */
748
+ function isTableDelimiterRow(line) {
749
+ let body = line.trim();
750
+ if (body.startsWith('|')) {
751
+ body = body.slice(1);
752
+ }
753
+ if (body.endsWith('|')) {
754
+ body = body.slice(0, -1);
755
+ }
756
+ return body.split('|').every(cell => /^:?-+:?$/.test(cell.trim()));
757
+ }
758
+
759
+ /**
760
+ * The ranges of `markdown` a code span must not start in: inline link
761
+ * destinations and titles, from the `(` after a `]` to its paired `)`. The
762
+ * link reads its destination before any code span there, as core and
763
+ * CommonMark read it.
764
+ */
765
+ function linkTargetRanges(markdown, code) {
766
+ const ranges = [];
767
+ if (!markdown.includes('](')) {
768
+ return ranges;
769
+ }
770
+ const pair = pairedDelimiters(markdown, code);
771
+ for (let index = markdown.indexOf(']('); index !== -1; index = markdown.indexOf('](', index + 1)) {
772
+ // An angle-bracket destination ends where core ends it: a parenthesis
773
+ // inside the brackets pairs with nothing.
774
+ const angleClose = angleDestinationEnd(markdown, index + 1);
775
+ const close = typeof angleClose === 'number' ? angleClose : pair[index + 1];
776
+ if (pair[index] !== -1 && close !== -1) {
777
+ ranges.push([index + 1, close + 1]);
778
+ }
779
+ }
780
+ return ranges;
781
+ }
782
+
783
+ /** The ranges of `markdown` that are GFM table rows, header row included. */
784
+ function tableRowRanges(markdown) {
785
+ const ranges = [];
786
+ const lines = markdown.split('\n');
787
+ const starts = [];
788
+ let offset = 0;
789
+ for (const line of lines) {
790
+ starts.push(offset);
791
+ offset += line.length + 1;
792
+ }
793
+ for (let index = 1; index < lines.length; index++) {
794
+ const line = lines[index];
795
+ if (line.includes('|') && isTableDelimiterRow(line) && lines[index - 1].includes('|')) {
796
+ let end = index + 1;
797
+ while (end < lines.length && lines[end].trim() !== '') {
798
+ end++;
799
+ }
800
+ ranges.push([starts[index - 1], starts[end - 1] + lines[end - 1].length]);
801
+ index = end;
802
+ }
803
+ }
804
+ return ranges;
805
+ }
806
+
807
+ /**
808
+ * Returns `markdown` with each code span outside fenced code replaced by a
809
+ * private-use stand-in, and the code each stands for, as core Markdown reads
810
+ * it. Code spans bind tighter than emphasis and links (CommonMark 0.31
811
+ * §6.1), but Lexical's import applies a code span before an earlier mark on
812
+ * the same line, which breaks every mark around code that follows another
813
+ * (`~~a~~ ~~`c`~~`); with the code out of the way, the marks pair first and
814
+ * the code comes back inside them. Backticks in a link's destination or
815
+ * title are the link's, never a code span: they become stand-ins too, put
816
+ * back into the link's address and title after import, so Lexical cannot
817
+ * pair them into code either. In a table row, a span holding an unescaped
818
+ * `|` is left alone, so the row still splits its cells there, as GFM does.
819
+ */
820
+ export function protectCodeSpans(markdown) {
821
+ const spans = new Map();
822
+ const standIns = new Map();
823
+ if (!markdown.includes('`')) {
824
+ return {
825
+ markdown,
826
+ spans,
827
+ standIns
828
+ };
829
+ }
830
+ const code = codeRangesByKind(markdown);
831
+ const targets = linkTargetRanges(markdown, [...code.fenced, ...code.spans]);
832
+ const rows = tableRowRanges(markdown);
833
+ // Whether a position lies in one of ranges sorted by start, for positions
834
+ // asked in order: one pass over the ranges in all.
835
+ const cursor = ranges => {
836
+ let next = 0;
837
+ let coveredTo = -1;
838
+ return position => {
839
+ while (next < ranges.length && ranges[next][0] <= position) {
840
+ coveredTo = Math.max(coveredTo, ranges[next][1]);
841
+ next++;
842
+ }
843
+ return position < coveredTo;
844
+ };
845
+ };
846
+ const inTarget = cursor(targets);
847
+ const inRow = cursor(rows);
848
+ // Replacements in source order: code spans, and backticks in targets.
849
+ const replacements = [];
850
+ for (const [start, end] of code.spans) {
851
+ const source = markdown.slice(start, end);
852
+ const isInRow = inRow(start);
853
+ if (inTarget(start) || isInRow && hasUnescapedPipe(source)) {
854
+ continue;
855
+ }
856
+ const [only, ...rest] = parseInlineAst(source);
857
+ if (rest.length > 0 || only?.type !== 'inlineCode') {
858
+ continue;
859
+ }
860
+ replacements.push({
861
+ start,
862
+ end,
863
+ code: only.value
864
+ });
865
+ }
866
+ for (const [from, to] of targets) {
867
+ for (let index = markdown.indexOf('`', from); index !== -1 && index < to; index = markdown.indexOf('`', index + 1)) {
868
+ replacements.push({
869
+ start: index,
870
+ end: index + 1,
871
+ code: null
872
+ });
873
+ }
874
+ }
875
+ replacements.sort((a, b) => a.start - b.start);
876
+ const available = absentCharacters(markdown);
877
+ let output = '';
878
+ let copied = 0;
879
+ for (const {
880
+ start,
881
+ end,
882
+ code: content
883
+ } of replacements) {
884
+ if (start < copied) {
885
+ continue;
886
+ }
887
+ const next = available.next();
888
+ if (next.done === true) {
889
+ break;
890
+ }
891
+ if (content == null) {
892
+ standIns.set(next.value, {
893
+ kind: 'literal',
894
+ text: '`'
895
+ });
896
+ } else {
897
+ spans.set(next.value, content);
898
+ }
899
+ output += markdown.slice(copied, start) + next.value;
900
+ copied = end;
901
+ }
902
+ return {
903
+ markdown: output + markdown.slice(copied),
904
+ spans,
905
+ standIns
906
+ };
907
+ }
908
+
909
+ /**
910
+ * Puts each code span back where its stand-in sits under `node`: the code as
911
+ * text with the code format, inside whatever marks the stand-in took. In a
912
+ * table cell an escaped `\|` in code is a `|`, as GFM reads cells.
913
+ */
914
+ export function $restoreCodeSpans(node, spans) {
915
+ if (spans.size === 0) {
916
+ return;
917
+ }
918
+ for (const text of node.getAllTextNodes()) {
919
+ const content = text.getTextContent();
920
+ const offsets = [];
921
+ let offset = 0;
922
+ for (const character of content) {
923
+ if (spans.has(character)) {
924
+ offsets.push(offset, offset + character.length);
925
+ }
926
+ offset += character.length;
927
+ }
928
+ if (offsets.length === 0) {
929
+ continue;
930
+ }
931
+ const pieces = text.splitText(...offsets.filter(point => point > 0 && point < content.length));
932
+ for (const piece of pieces) {
933
+ const code = spans.get(piece.getTextContent());
934
+ if (code == null) {
935
+ continue;
936
+ }
937
+ const inCell = $findMatchingParent(piece, $isTableCellNode) != null;
938
+ piece.setTextContent(inCell ? code.replace(/\\\|/g, '|') : code);
939
+ if (!piece.hasFormat('code')) {
940
+ piece.toggleFormat('code');
941
+ }
942
+ }
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Returns `markdown` with every character reference outside code replaced by
948
+ * a private-use stand-in that occurs nowhere else in it, and the escaped `&`
949
+ * of an escaped reference replaced by one too. One pass over the source.
950
+ */
951
+ export function protectCharacterReferences(markdown) {
952
+ const standIns = new Map();
953
+ if (!markdown.includes('&')) {
954
+ return {
955
+ markdown,
956
+ standIns
957
+ };
958
+ }
959
+ const available = absentCharacters(markdown);
960
+ const byReference = new Map();
961
+ let ampersand = null;
962
+ const standInFor = reference => {
963
+ const existing = reference == null ? ampersand : byReference.get(reference);
964
+ if (existing != null) {
965
+ return existing;
966
+ }
967
+ const next = available.next();
968
+ if (next.done === true) {
969
+ // Every private-use character is in the text or already used: leave
970
+ // the rest as written.
971
+ return null;
972
+ }
973
+ if (reference == null) {
974
+ ampersand = next.value;
975
+ standIns.set(next.value, {
976
+ kind: 'ampersand'
977
+ });
978
+ } else {
979
+ byReference.set(reference, next.value);
980
+ standIns.set(next.value, {
981
+ kind: 'reference',
982
+ source: reference
983
+ });
984
+ }
985
+ return next.value;
986
+ };
987
+ const code = codeRanges(markdown);
988
+ let output = '';
989
+ let copied = 0;
990
+ let codeIndex = 0;
991
+ let index = markdown.indexOf('&');
992
+ while (index !== -1) {
993
+ while (code[codeIndex] != null && code[codeIndex][1] <= index) {
994
+ codeIndex++;
995
+ }
996
+ const range = code[codeIndex];
997
+ if (range != null && range[0] <= index) {
998
+ index = markdown.indexOf('&', range[1]);
999
+ continue;
1000
+ }
1001
+ REFERENCE_LIKE.lastIndex = index;
1002
+ const match = REFERENCE_LIKE.exec(markdown);
1003
+ if (match == null) {
1004
+ index = markdown.indexOf('&', index + 1);
1005
+ continue;
1006
+ }
1007
+ let backslashes = 0;
1008
+ while (markdown[index - 1 - backslashes] === '\\') {
1009
+ backslashes++;
1010
+ }
1011
+ const reference = match[0];
1012
+ const end = index + reference.length;
1013
+ if (backslashes % 2 === 1) {
1014
+ // An escaped `&`: the reference stays literal, without its backslash.
1015
+ const standIn = standInFor(null);
1016
+ if (standIn != null) {
1017
+ output += markdown.slice(copied, index - 1) + standIn;
1018
+ copied = index + 1;
1019
+ }
1020
+ } else if (decodeMarkdownCharacterReferences(reference) !== reference) {
1021
+ const standIn = standInFor(reference);
1022
+ if (standIn != null) {
1023
+ output += markdown.slice(copied, index) + standIn;
1024
+ copied = end;
1025
+ }
1026
+ } else if (reference.startsWith('&#')) {
1027
+ // Not a reference (too many digits): literal, but kept from Lexical's
1028
+ // own numeric decoding.
1029
+ const standIn = standInFor(null);
1030
+ if (standIn != null) {
1031
+ output += markdown.slice(copied, index) + standIn;
1032
+ copied = index + 1;
1033
+ }
1034
+ }
1035
+ index = markdown.indexOf('&', end);
1036
+ }
1037
+ return {
1038
+ markdown: output + markdown.slice(copied),
1039
+ standIns
1040
+ };
1041
+ }
1042
+
1043
+ /** `text` with each stand-in replaced as `replace` says. */
1044
+ function restored(text, standIns, replace) {
1045
+ let output = '';
1046
+ let changed = false;
1047
+ for (const character of text) {
1048
+ const standIn = standIns.get(character);
1049
+ if (standIn == null) {
1050
+ output += character;
1051
+ } else {
1052
+ output += replace(standIn);
1053
+ changed = true;
1054
+ }
1055
+ }
1056
+ return changed ? output : text;
1057
+ }
1058
+ const decoded = standIn => {
1059
+ switch (standIn.kind) {
1060
+ case 'literal':
1061
+ return standIn.text;
1062
+ case 'ampersand':
1063
+ return '&';
1064
+ case 'backslash':
1065
+ return '\\';
1066
+ case 'escape':
1067
+ return standIn.character;
1068
+ case 'reference':
1069
+ return decodeMarkdownCharacterReferences(standIn.source);
1070
+ }
1071
+ };
1072
+ const asWritten = standIn => {
1073
+ switch (standIn.kind) {
1074
+ case 'literal':
1075
+ return standIn.text;
1076
+ case 'ampersand':
1077
+ return '&';
1078
+ case 'backslash':
1079
+ return '\\';
1080
+ case 'escape':
1081
+ return `\\${standIn.character}`;
1082
+ case 'reference':
1083
+ return standIn.source;
1084
+ }
1085
+ };
1086
+
1087
+ /**
1088
+ * Replaces the stand-ins under `node` with the characters their references
1089
+ * name or their escapes stand for — or, in code Lexical read as code, with
1090
+ * the references and escapes as written — in text, link destinations, and
1091
+ * link titles. One pass over the text.
1092
+ */
1093
+ export function $restoreCharacterReferences(node, standIns) {
1094
+ if (standIns.size === 0) {
1095
+ return;
1096
+ }
1097
+ const visit = (current, inCode) => {
1098
+ if ($isTextNode(current)) {
1099
+ const text = current.getTextContent();
1100
+ const next = restored(text, standIns, inCode || current.hasFormat('code') ? asWritten : decoded);
1101
+ if (next !== text) {
1102
+ current.setTextContent(next);
1103
+ }
1104
+ return;
1105
+ }
1106
+ if ($isLinkNode(current)) {
1107
+ const url = restored(current.getURL(), standIns, decoded);
1108
+ if (url !== current.getURL()) {
1109
+ current.setURL(url);
1110
+ }
1111
+ const title = current.getTitle();
1112
+ if (title != null) {
1113
+ const nextTitle = restored(title, standIns, decoded);
1114
+ if (nextTitle !== title) {
1115
+ current.setTitle(nextTitle);
1116
+ }
1117
+ }
1118
+ }
1119
+ if ($isElementNode(current)) {
1120
+ const isCode = inCode || $isCodeNode(current);
1121
+ for (const child of current.getChildren()) {
1122
+ visit(child, isCode);
1123
+ }
1124
+ }
1125
+ };
1126
+ visit(node, false);
1127
+ }