@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,493 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file CodeSyntaxPlugin.tsx
7
+ * @input Uses @lexical/react (composer context), @lexical/code (CodeNode),
8
+ * lexical, core CodeBlock's highlight styles and token types, and
9
+ * CodeLineTokens (codeLineTokens.ts), core CodeBlock's tokens kept line by
10
+ * line.
11
+ * @output Exports CodeSyntaxPlugin, which colors the code in every fenced code
12
+ * block with core CodeBlock's tokenizer and syntax color tokens; and
13
+ * CODE_SYNTAX_CLASS, the class the theme gives code blocks for it.
14
+ * @position Rendered by RichTextEditor and RichTextView. A fenced code block
15
+ * with a language core CodeBlock knows is colored as core Markdown colors
16
+ * it: the same tokenizer finds the same tokens, and each token type takes
17
+ * its `--color-syntax-*` token (spec:AST-061 FR1) — no palette of its own.
18
+ * The colors are CSS Custom Highlight ranges over the text Lexical renders,
19
+ * the way core CodeBlock colors editable code, so the document and its DOM
20
+ * are untouched. A block is colored in full when it appears, or when its
21
+ * language, its element, or its set of text nodes changes. An edit that
22
+ * only rewrites one text node's text re-tokenizes just the lines
23
+ * CodeLineTokens says it can change, and replaces only the ranges in that
24
+ * text node, reading no other. Browsers without the Highlight API show
25
+ * plain code.
26
+ */
27
+
28
+ import {useEffect, useState} from 'react';
29
+ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
30
+ import {$isCodeNode, CodeNode} from '@lexical/code';
31
+ import {
32
+ $getNodeByKey,
33
+ $isTextNode,
34
+ type LexicalEditor,
35
+ type NodeKey,
36
+ type NodeMutation,
37
+ } from 'lexical';
38
+ import {
39
+ ensureHighlightStyles,
40
+ TOKEN_TYPES,
41
+ type CodeLanguage,
42
+ } from '@blue-patterns/core/CodeBlock';
43
+ import {listedLanguage, sameLanguages} from '../codeLanguages';
44
+ import {CodeLineTokens} from '../codeLineTokens';
45
+
46
+ /** The class the theme adds to every code block (editorTheme.ts). */
47
+ export const CODE_SYNTAX_CLASS = 'astryx-richtext-code';
48
+
49
+ let hasInjectedRules = false;
50
+
51
+ /**
52
+ * The highlight rules for code blocks, injected once: each token type takes
53
+ * the same syntax color token core CodeBlock's rules use.
54
+ */
55
+ function ensureRules(): void {
56
+ if (hasInjectedRules || typeof document === 'undefined') {
57
+ return;
58
+ }
59
+ hasInjectedRules = true;
60
+ const style = document.createElement('style');
61
+ style.setAttribute('data-astryx-richtext-code-syntax', '');
62
+ style.textContent = TOKEN_TYPES.map(
63
+ type =>
64
+ `.${CODE_SYNTAX_CLASS}::highlight(astryx-${type}) { color: var(--color-syntax-${type}); }`,
65
+ ).join('\n');
66
+ document.head.append(style);
67
+ }
68
+
69
+ function supportsHighlights(): boolean {
70
+ return (
71
+ typeof CSS !== 'undefined' &&
72
+ 'highlights' in CSS &&
73
+ typeof Highlight !== 'undefined'
74
+ );
75
+ }
76
+
77
+ /** A colored token: its range, and the highlight that holds it. */
78
+ interface Painted {
79
+ readonly range: Range;
80
+ readonly highlight: Highlight;
81
+ }
82
+
83
+ /**
84
+ * A colored code block, with what an edit needs to recolor part of it.
85
+ * Offsets in the rendered text are offsets in the block's DOM text nodes
86
+ * laid end to end, which is the code itself when its line breaks render as
87
+ * text, and the code without them when each renders as an element (`flat`).
88
+ */
89
+ interface Colored {
90
+ readonly element: HTMLElement;
91
+ readonly language: string;
92
+ readonly lines: CodeLineTokens;
93
+ readonly flat: boolean;
94
+ /** The block's DOM text nodes in order. */
95
+ readonly texts: Text[];
96
+ /** Where each DOM text node starts in the rendered text. */
97
+ readonly starts: number[];
98
+ /** Where each leaf's DOM text node starts in the code (NaN for others). */
99
+ readonly codeStarts: number[];
100
+ /** The rendered text's length. */
101
+ length: number;
102
+ /** The Lexical text nodes that render as one DOM text node each, by key:
103
+ * that node's index in `texts`, and the text last colored. */
104
+ readonly leaves: Map<NodeKey, {readonly index: number; text: string}>;
105
+ /** Each line's colored tokens. */
106
+ ranges: Painted[][];
107
+ }
108
+
109
+ function highlightFor(type: string): Highlight {
110
+ const name = `astryx-${type}`;
111
+ let highlight = CSS.highlights.get(name);
112
+ if (highlight == null) {
113
+ highlight = new Highlight();
114
+ CSS.highlights.set(name, highlight);
115
+ }
116
+ return highlight;
117
+ }
118
+
119
+ /**
120
+ * The DOM position of `offset` in a block's rendered text: in the text node
121
+ * that holds the character there when `inside`, else in the first text node
122
+ * that reaches it. Null past the end of the text.
123
+ */
124
+ function pointAt(
125
+ block: Colored,
126
+ offset: number,
127
+ inside: boolean,
128
+ ): [Text, number] | null {
129
+ const {texts, starts, length} = block;
130
+ if (texts.length === 0 || offset > length) {
131
+ return null;
132
+ }
133
+ let low = 0;
134
+ let high = texts.length - 1;
135
+ while (low < high) {
136
+ const mid = (low + high) >>> 1;
137
+ const end = mid + 1 < texts.length ? starts[mid + 1] : length;
138
+ if (inside ? end > offset : end >= offset) {
139
+ high = mid;
140
+ } else {
141
+ low = mid + 1;
142
+ }
143
+ }
144
+ return [texts[low], offset - starts[low]];
145
+ }
146
+
147
+ /** Colors one line's tokens. */
148
+ function paintLine(block: Colored, line: number): void {
149
+ // A flat block's rendered text has no line breaks before this line.
150
+ const base = block.lines.starts[line] - (block.flat ? line : 0);
151
+ const painted: Painted[] = [];
152
+ for (const token of block.lines.tokens[line] ?? []) {
153
+ const start = base + token.start;
154
+ const end = base + token.end;
155
+ const from = pointAt(block, start, start < end);
156
+ const to = pointAt(block, end, false);
157
+ if (from == null || to == null) {
158
+ continue;
159
+ }
160
+ const highlight = highlightFor(token.type);
161
+ try {
162
+ const range = new Range();
163
+ range.setStart(...from);
164
+ range.setEnd(...to);
165
+ highlight.add(range);
166
+ painted.push({range, highlight});
167
+ } catch {
168
+ // A range the DOM refuses colors nothing.
169
+ }
170
+ }
171
+ block.ranges[line] = painted;
172
+ }
173
+
174
+ function unpaintLine(block: Colored, line: number): void {
175
+ for (const {range, highlight} of block.ranges[line] ?? []) {
176
+ highlight.delete(range);
177
+ }
178
+ block.ranges[line] = [];
179
+ }
180
+
181
+ function unpaintAll(block: Colored): void {
182
+ for (let line = 0; line < block.ranges.length; line++) {
183
+ unpaintLine(block, line);
184
+ }
185
+ block.ranges = [];
186
+ }
187
+
188
+ function repaintAll(block: Colored): void {
189
+ unpaintAll(block);
190
+ for (let line = 0; line < block.lines.tokens.length; line++) {
191
+ paintLine(block, line);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Lists a block's DOM text nodes in order with their offsets, and the
197
+ * Lexical text nodes that render as exactly one of them.
198
+ */
199
+ function $indexText(
200
+ editor: LexicalEditor,
201
+ node: CodeNode,
202
+ block: Colored,
203
+ ): void {
204
+ const walker = document.createTreeWalker(block.element, NodeFilter.SHOW_TEXT);
205
+ while (walker.nextNode() != null) {
206
+ const text = walker.currentNode as Text;
207
+ block.texts.push(text);
208
+ block.starts.push(block.length);
209
+ block.codeStarts.push(NaN);
210
+ block.length += text.length;
211
+ }
212
+ const indexes = new Map(block.texts.map((text, index) => [text, index]));
213
+ let codeStart = 0;
214
+ for (const child of node.getChildren()) {
215
+ const dom = $isTextNode(child)
216
+ ? editor.getElementByKey(child.getKey())
217
+ : null;
218
+ const index =
219
+ dom?.childNodes.length === 1
220
+ ? indexes.get(dom.firstChild as Text)
221
+ : undefined;
222
+ if (index != null) {
223
+ block.leaves.set(child.getKey(), {index, text: child.getTextContent()});
224
+ block.codeStarts[index] = codeStart;
225
+ }
226
+ codeStart += child.getTextContentSize();
227
+ }
228
+ }
229
+
230
+ /** The key of the code block that holds the node `key`, if any. */
231
+ function $codeBlockOf(key: NodeKey): NodeKey | null {
232
+ let node = $getNodeByKey(key)?.getParent() ?? null;
233
+ while (node != null && !$isCodeNode(node)) {
234
+ node = node.getParent();
235
+ }
236
+ return node?.getKey() ?? null;
237
+ }
238
+
239
+ export function CodeSyntaxPlugin({
240
+ codeLanguages,
241
+ }: {
242
+ /** Language values a fence may name (component:Markdown FR30). */
243
+ codeLanguages?: ReadonlyArray<CodeLanguage>;
244
+ } = {}): null {
245
+ const [editor] = useLexicalComposerContext();
246
+ // Keep one list while it names the same values, so an inline
247
+ // `codeLanguages={[…]}` does not re-colour every block on each parent
248
+ // render (adjusting state during render, not in an Effect).
249
+ const [languages, setLanguages] = useState(codeLanguages);
250
+ if (!sameLanguages(languages, codeLanguages)) {
251
+ setLanguages(codeLanguages);
252
+ }
253
+
254
+ // Synchronizes the editor's code blocks with the CSS highlight registry:
255
+ // Lexical mutation and update listeners colour each block, and the cleanup
256
+ // removes the listeners and every highlight range they added.
257
+ useEffect(() => {
258
+ if (!supportsHighlights()) {
259
+ return undefined;
260
+ }
261
+ ensureRules();
262
+ // Every code block in the editor, the ones colored, and the ones colored
263
+ // in full during the current update.
264
+ const blocks = new Set<NodeKey>();
265
+ const colored = new Map<NodeKey, Colored>();
266
+ const repainted = new Set<NodeKey>();
267
+ const uncolor = (key: NodeKey) => {
268
+ const block = colored.get(key);
269
+ if (block != null) {
270
+ unpaintAll(block);
271
+ colored.delete(key);
272
+ }
273
+ };
274
+ // Colors a block in full, unless `force` is false and its element,
275
+ // language, and code are as last colored.
276
+ const $color = (key: NodeKey, force: boolean) => {
277
+ const node = $getNodeByKey(key);
278
+ const element = editor.getElementByKey(key);
279
+ if (!$isCodeNode(node) || element == null) {
280
+ uncolor(key);
281
+ return;
282
+ }
283
+ const code = node.getTextContent();
284
+ const language = node.getLanguage() ?? '';
285
+ const previous = colored.get(key);
286
+ if (
287
+ !force &&
288
+ previous != null &&
289
+ previous.element === element &&
290
+ previous.language === language &&
291
+ previous.lines.text === code
292
+ ) {
293
+ return;
294
+ }
295
+ uncolor(key);
296
+ const rendered = element.textContent ?? '';
297
+ if (rendered !== code && rendered !== code.replaceAll('\n', '')) {
298
+ // Not rendered yet; the block's next change tries again.
299
+ return;
300
+ }
301
+ ensureHighlightStyles();
302
+ const block: Colored = {
303
+ element,
304
+ language,
305
+ lines: new CodeLineTokens(
306
+ code,
307
+ listedLanguage(languages, language) ?? language,
308
+ ),
309
+ flat: rendered !== code,
310
+ texts: [],
311
+ starts: [],
312
+ codeStarts: [],
313
+ length: 0,
314
+ leaves: new Map(),
315
+ ranges: [],
316
+ };
317
+ $indexText(editor, node, block);
318
+ repaintAll(block);
319
+ colored.set(key, block);
320
+ repainted.add(key);
321
+ };
322
+ // Recolors a block after an update that rewrote the text of its leaf
323
+ // `leafKey` and changed nothing else in it. False when the block must
324
+ // be colored in full instead.
325
+ const $recolorLeaf = (key: NodeKey, leafKey: NodeKey): boolean => {
326
+ const block = colored.get(key);
327
+ const leaf = block?.leaves.get(leafKey);
328
+ const node = $getNodeByKey(leafKey);
329
+ const parent = node?.getParent();
330
+ if (
331
+ block == null ||
332
+ leaf == null ||
333
+ !$isTextNode(node) ||
334
+ !$isCodeNode(parent) ||
335
+ parent.getKey() !== key ||
336
+ (parent.getLanguage() ?? '') !== block.language ||
337
+ editor.getElementByKey(key) !== block.element
338
+ ) {
339
+ return false;
340
+ }
341
+ const text = block.texts[leaf.index];
342
+ const dom = editor.getElementByKey(leafKey);
343
+ const after = node.getTextContent();
344
+ if (
345
+ dom?.childNodes.length !== 1 ||
346
+ dom.firstChild !== text ||
347
+ text.data !== after
348
+ ) {
349
+ return false;
350
+ }
351
+ const before = leaf.text;
352
+ if (after === before) {
353
+ return true;
354
+ }
355
+ let first = 0;
356
+ while (
357
+ first < before.length &&
358
+ first < after.length &&
359
+ before[first] === after[first]
360
+ ) {
361
+ first++;
362
+ }
363
+ let beforeEnd = before.length;
364
+ let afterEnd = after.length;
365
+ while (
366
+ beforeEnd > first &&
367
+ afterEnd > first &&
368
+ before[beforeEnd - 1] === after[afterEnd - 1]
369
+ ) {
370
+ beforeEnd--;
371
+ afterEnd--;
372
+ }
373
+ const inserted = after.slice(first, afterEnd);
374
+ // A flat block's text holds no line breaks; one typed in would no
375
+ // longer lay the code over its rendered text.
376
+ if (block.flat && inserted.includes('\n')) {
377
+ return false;
378
+ }
379
+ const leafStart = block.codeStarts[leaf.index];
380
+ const lines = block.lines.replace(
381
+ leafStart + first,
382
+ beforeEnd - first,
383
+ inserted,
384
+ );
385
+ const delta = after.length - before.length;
386
+ for (let i = leaf.index + 1; i < block.texts.length; i++) {
387
+ block.starts[i] += delta;
388
+ block.codeStarts[i] += delta;
389
+ }
390
+ block.length += delta;
391
+ leaf.text = after;
392
+ if (lines == null) {
393
+ repaintAll(block);
394
+ return true;
395
+ }
396
+ // Lexical rewrites a changed DOM text node whole, which leaves every
397
+ // range in it stale: recolor each line the node holds, and any other
398
+ // line the edit re-tokenized. Every other range sits in a text node of
399
+ // its own line and is still right.
400
+ const recolor = new Set(lines);
401
+ const last = block.lines.lineAt(
402
+ leafStart + Math.max(after.length - 1, 0),
403
+ );
404
+ for (let i = block.lines.lineAt(leafStart); i <= last; i++) {
405
+ recolor.add(i);
406
+ }
407
+ for (const i of recolor) {
408
+ unpaintLine(block, i);
409
+ paintLine(block, i);
410
+ }
411
+ return true;
412
+ };
413
+ const unregisterMutations = editor.registerMutationListener(
414
+ CodeNode,
415
+ mutations => {
416
+ editor.getEditorState().read(() => {
417
+ for (const [key, mutation] of mutations) {
418
+ if (mutation === 'destroyed') {
419
+ blocks.delete(key);
420
+ uncolor(key);
421
+ } else {
422
+ blocks.add(key);
423
+ $color(key, false);
424
+ }
425
+ }
426
+ });
427
+ },
428
+ {skipInitialization: false},
429
+ );
430
+ repainted.clear();
431
+ // Typing in a block changes its text nodes, not the block itself: after
432
+ // each update, recolor the blocks whose text nodes changed. A block whose
433
+ // only change is one leaf's text takes the partial path; any other
434
+ // change colors it in full, unless the block listener just did.
435
+ const unregisterUpdates = editor.registerUpdateListener(
436
+ ({editorState, mutatedNodes, prevEditorState}) => {
437
+ if (mutatedNodes != null && blocks.size > 0) {
438
+ const mutations: Array<[NodeKey, NodeMutation]> = [];
439
+ for (const nodes of mutatedNodes.values()) {
440
+ for (const mutation of nodes) {
441
+ mutations.push(mutation);
442
+ }
443
+ }
444
+ const before = prevEditorState.read(() =>
445
+ mutations.map(([key, mutation]) =>
446
+ mutation === 'created' ? null : $codeBlockOf(key),
447
+ ),
448
+ );
449
+ editorState.read(() => {
450
+ // Each changed block, with the one leaf whose text an update
451
+ // rewrote, or null when anything else in it changed.
452
+ const changed = new Map<NodeKey, NodeKey | null>();
453
+ const note = (key: NodeKey | null, leafKey: NodeKey | null) => {
454
+ if (key != null && blocks.has(key)) {
455
+ changed.set(key, changed.has(key) ? null : leafKey);
456
+ }
457
+ };
458
+ mutations.forEach(([key, mutation], i) => {
459
+ const after = mutation === 'destroyed' ? null : $codeBlockOf(key);
460
+ const rewritten =
461
+ mutation === 'updated' &&
462
+ after === before[i] &&
463
+ $isTextNode($getNodeByKey(key));
464
+ note(after, rewritten ? key : null);
465
+ if (before[i] !== after) {
466
+ note(before[i], null);
467
+ }
468
+ });
469
+ for (const [key, leafKey] of changed) {
470
+ if (
471
+ !repainted.has(key) &&
472
+ (leafKey == null || !$recolorLeaf(key, leafKey))
473
+ ) {
474
+ $color(key, true);
475
+ }
476
+ }
477
+ });
478
+ }
479
+ repainted.clear();
480
+ },
481
+ );
482
+ return () => {
483
+ unregisterMutations();
484
+ unregisterUpdates();
485
+ for (const key of [...colored.keys()]) {
486
+ uncolor(key);
487
+ }
488
+ blocks.clear();
489
+ };
490
+ }, [editor, languages]);
491
+
492
+ return null;
493
+ }