@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.
- package/CHANGELOG.md +275 -0
- package/LICENSE +22 -0
- package/README.md +88 -2
- package/astryx.integration.mjs +11 -0
- package/blocks/RichTextEditorShowcase.template.mjs +13 -0
- package/blocks/RichTextEditorShowcase.tsx +19 -0
- package/dist/RichTextEditor.d.ts +282 -0
- package/dist/RichTextEditor.d.ts.map +1 -0
- package/dist/RichTextEditor.js +657 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
- package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
- package/dist/RichTextEditorToolbar.d.ts +186 -0
- package/dist/RichTextEditorToolbar.d.ts.map +1 -0
- package/dist/RichTextEditorToolbar.js +688 -0
- package/dist/RichTextView.d.ts +92 -0
- package/dist/RichTextView.d.ts.map +1 -0
- package/dist/RichTextView.js +267 -0
- package/dist/codeLanguages.d.ts +14 -0
- package/dist/codeLanguages.d.ts.map +1 -0
- package/dist/codeLanguages.js +34 -0
- package/dist/codeLineTokens.d.ts +53 -0
- package/dist/codeLineTokens.d.ts.map +1 -0
- package/dist/codeLineTokens.js +188 -0
- package/dist/editorNodes.d.ts +8 -0
- package/dist/editorNodes.d.ts.map +1 -0
- package/dist/editorNodes.js +30 -0
- package/dist/editorTheme.d.ts +7 -0
- package/dist/editorTheme.d.ts.map +1 -0
- package/dist/editorTheme.js +328 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
- package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
- package/dist/internal/CodeBackspacePlugin.js +70 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
- package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
- package/dist/internal/CodePastePlugin.d.ts +5 -0
- package/dist/internal/CodePastePlugin.d.ts.map +1 -0
- package/dist/internal/CodePastePlugin.js +104 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
- package/dist/internal/CodeSyntaxPlugin.js +397 -0
- package/dist/internal/CodeTabPlugin.d.ts +2 -0
- package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
- package/dist/internal/CodeTabPlugin.js +256 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
- package/dist/internal/ImageSourceCaretPlugin.js +105 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
- package/dist/internal/ListItemShortcutsPlugin.js +314 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
- package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
- package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
- package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
- package/dist/internal/SelectAllScopePlugin.js +67 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
- package/dist/internal/TableColumnFloorPlugin.js +84 -0
- package/dist/internal/TablePastePlugin.d.ts +9 -0
- package/dist/internal/TablePastePlugin.d.ts.map +1 -0
- package/dist/internal/TablePastePlugin.js +183 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
- package/dist/internal/TableScrollRegionPlugin.js +95 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
- package/dist/internal/TaskCheckboxPlugin.js +193 -0
- package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
- package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
- package/dist/internal/ToolbarLinkPopover.js +292 -0
- package/dist/linkEditing.d.ts +6 -0
- package/dist/linkEditing.d.ts.map +1 -0
- package/dist/linkEditing.js +20 -0
- package/dist/linkForm.d.ts +28 -0
- package/dist/linkForm.d.ts.map +1 -0
- package/dist/linkForm.js +137 -0
- package/dist/linkUtils.d.ts +27 -0
- package/dist/linkUtils.d.ts.map +1 -0
- package/dist/linkUtils.js +101 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +17 -0
- package/dist/markdownCharacterReferences.d.ts +135 -0
- package/dist/markdownCharacterReferences.d.ts.map +1 -0
- package/dist/markdownCharacterReferences.js +1127 -0
- package/dist/markdownCodeFence.d.ts +4 -0
- package/dist/markdownCodeFence.d.ts.map +1 -0
- package/dist/markdownCodeFence.js +167 -0
- package/dist/markdownExtensionNode.d.ts +88 -0
- package/dist/markdownExtensionNode.d.ts.map +1 -0
- package/dist/markdownExtensionNode.js +248 -0
- package/dist/markdownExtensions.d.ts +115 -0
- package/dist/markdownExtensions.d.ts.map +1 -0
- package/dist/markdownExtensions.js +392 -0
- package/dist/markdownHardLineBreak.d.ts +25 -0
- package/dist/markdownHardLineBreak.d.ts.map +1 -0
- package/dist/markdownHardLineBreak.js +59 -0
- package/dist/markdownHeading.d.ts +20 -0
- package/dist/markdownHeading.d.ts.map +1 -0
- package/dist/markdownHeading.js +30 -0
- package/dist/markdownLink.d.ts +5 -0
- package/dist/markdownLink.d.ts.map +1 -0
- package/dist/markdownLink.js +80 -0
- package/dist/markdownListExport.d.ts +3 -0
- package/dist/markdownListExport.d.ts.map +1 -0
- package/dist/markdownListExport.js +68 -0
- package/dist/markdownListIndentation.d.ts +8 -0
- package/dist/markdownListIndentation.d.ts.map +1 -0
- package/dist/markdownListIndentation.js +101 -0
- package/dist/markdownOrderedList.d.ts +3 -0
- package/dist/markdownOrderedList.d.ts.map +1 -0
- package/dist/markdownOrderedList.js +51 -0
- package/dist/markdownQuote.d.ts +22 -0
- package/dist/markdownQuote.d.ts.map +1 -0
- package/dist/markdownQuote.js +32 -0
- package/dist/markdownSerializers.d.ts +49 -0
- package/dist/markdownSerializers.d.ts.map +1 -0
- package/dist/markdownSerializers.js +83 -0
- package/dist/markdownSource.d.ts +71 -0
- package/dist/markdownSource.d.ts.map +1 -0
- package/dist/markdownSource.js +804 -0
- package/dist/markdownSpaceRuns.d.ts +24 -0
- package/dist/markdownSpaceRuns.d.ts.map +1 -0
- package/dist/markdownSpaceRuns.js +147 -0
- package/dist/markdownTable.d.ts +35 -0
- package/dist/markdownTable.d.ts.map +1 -0
- package/dist/markdownTable.js +227 -0
- package/dist/markdownTaskList.d.ts +48 -0
- package/dist/markdownTaskList.d.ts.map +1 -0
- package/dist/markdownTaskList.js +166 -0
- package/dist/markdownThematicBreak.d.ts +3 -0
- package/dist/markdownThematicBreak.d.ts.map +1 -0
- package/dist/markdownThematicBreak.js +55 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
- package/dist/richtext.css +213 -0
- package/dist/textSemantics.d.ts +3 -0
- package/dist/textSemantics.d.ts.map +1 -0
- package/dist/textSemantics.js +87 -0
- package/package.json +158 -3
- package/src/RichTextEditor.doc.mjs +308 -0
- package/src/RichTextEditor.tsx +1109 -0
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
- package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
- package/src/RichTextEditorToolbar.doc.mjs +92 -0
- package/src/RichTextEditorToolbar.tsx +871 -0
- package/src/RichTextView.doc.mjs +101 -0
- package/src/RichTextView.tsx +351 -0
- package/src/codeLanguages.ts +42 -0
- package/src/codeLineTokens.ts +241 -0
- package/src/doc-mjs.d.ts +23 -0
- package/src/editorNodes.ts +44 -0
- package/src/editorTheme.ts +550 -0
- package/src/index.ts +59 -0
- package/src/internal/CodeBackspacePlugin.tsx +96 -0
- package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
- package/src/internal/CodePastePlugin.tsx +144 -0
- package/src/internal/CodeSyntaxPlugin.tsx +493 -0
- package/src/internal/CodeTabPlugin.tsx +298 -0
- package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
- package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
- package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
- package/src/internal/SelectAllScopePlugin.tsx +86 -0
- package/src/internal/TableColumnFloorPlugin.tsx +99 -0
- package/src/internal/TablePastePlugin.tsx +223 -0
- package/src/internal/TableScrollRegionPlugin.tsx +107 -0
- package/src/internal/TaskCheckboxPlugin.tsx +246 -0
- package/src/internal/ToolbarLinkPopover.tsx +392 -0
- package/src/linkEditing.ts +20 -0
- package/src/linkForm.tsx +156 -0
- package/src/linkUtils.ts +106 -0
- package/src/markdown.ts +25 -0
- package/src/markdownCharacterReferences.ts +1137 -0
- package/src/markdownCodeFence.ts +216 -0
- package/src/markdownExtensionNode.ts +339 -0
- package/src/markdownExtensions.ts +496 -0
- package/src/markdownHardLineBreak.ts +73 -0
- package/src/markdownHeading.ts +33 -0
- package/src/markdownLink.ts +86 -0
- package/src/markdownListExport.ts +85 -0
- package/src/markdownListIndentation.ts +105 -0
- package/src/markdownOrderedList.ts +60 -0
- package/src/markdownQuote.ts +35 -0
- package/src/markdownSerializers.ts +108 -0
- package/src/markdownSource.ts +1045 -0
- package/src/markdownSpaceRuns.ts +164 -0
- package/src/markdownTable.ts +309 -0
- package/src/markdownTaskList.ts +222 -0
- package/src/markdownThematicBreak.ts +66 -0
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
- package/src/textSemantics.ts +104 -0
|
@@ -0,0 +1,397 @@
|
|
|
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
|
+
import { useEffect, useState } from 'react';
|
|
28
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
29
|
+
import { $isCodeNode, CodeNode } from '@lexical/code';
|
|
30
|
+
import { $getNodeByKey, $isTextNode } from 'lexical';
|
|
31
|
+
import { ensureHighlightStyles, TOKEN_TYPES } from '@blue-patterns/core/CodeBlock';
|
|
32
|
+
import { listedLanguage, sameLanguages } from "../codeLanguages.js";
|
|
33
|
+
import { CodeLineTokens } from "../codeLineTokens.js";
|
|
34
|
+
|
|
35
|
+
/** The class the theme adds to every code block (editorTheme.ts). */
|
|
36
|
+
export const CODE_SYNTAX_CLASS = 'astryx-richtext-code';
|
|
37
|
+
let hasInjectedRules = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The highlight rules for code blocks, injected once: each token type takes
|
|
41
|
+
* the same syntax color token core CodeBlock's rules use.
|
|
42
|
+
*/
|
|
43
|
+
function ensureRules() {
|
|
44
|
+
if (hasInjectedRules || typeof document === 'undefined') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
hasInjectedRules = true;
|
|
48
|
+
const style = document.createElement('style');
|
|
49
|
+
style.setAttribute('data-astryx-richtext-code-syntax', '');
|
|
50
|
+
style.textContent = TOKEN_TYPES.map(type => `.${CODE_SYNTAX_CLASS}::highlight(astryx-${type}) { color: var(--color-syntax-${type}); }`).join('\n');
|
|
51
|
+
document.head.append(style);
|
|
52
|
+
}
|
|
53
|
+
function supportsHighlights() {
|
|
54
|
+
return typeof CSS !== 'undefined' && 'highlights' in CSS && typeof Highlight !== 'undefined';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A colored token: its range, and the highlight that holds it. */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A colored code block, with what an edit needs to recolor part of it.
|
|
61
|
+
* Offsets in the rendered text are offsets in the block's DOM text nodes
|
|
62
|
+
* laid end to end, which is the code itself when its line breaks render as
|
|
63
|
+
* text, and the code without them when each renders as an element (`flat`).
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
function highlightFor(type) {
|
|
67
|
+
const name = `astryx-${type}`;
|
|
68
|
+
let highlight = CSS.highlights.get(name);
|
|
69
|
+
if (highlight == null) {
|
|
70
|
+
highlight = new Highlight();
|
|
71
|
+
CSS.highlights.set(name, highlight);
|
|
72
|
+
}
|
|
73
|
+
return highlight;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The DOM position of `offset` in a block's rendered text: in the text node
|
|
78
|
+
* that holds the character there when `inside`, else in the first text node
|
|
79
|
+
* that reaches it. Null past the end of the text.
|
|
80
|
+
*/
|
|
81
|
+
function pointAt(block, offset, inside) {
|
|
82
|
+
const {
|
|
83
|
+
texts,
|
|
84
|
+
starts,
|
|
85
|
+
length
|
|
86
|
+
} = block;
|
|
87
|
+
if (texts.length === 0 || offset > length) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let low = 0;
|
|
91
|
+
let high = texts.length - 1;
|
|
92
|
+
while (low < high) {
|
|
93
|
+
const mid = low + high >>> 1;
|
|
94
|
+
const end = mid + 1 < texts.length ? starts[mid + 1] : length;
|
|
95
|
+
if (inside ? end > offset : end >= offset) {
|
|
96
|
+
high = mid;
|
|
97
|
+
} else {
|
|
98
|
+
low = mid + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [texts[low], offset - starts[low]];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Colors one line's tokens. */
|
|
105
|
+
function paintLine(block, line) {
|
|
106
|
+
// A flat block's rendered text has no line breaks before this line.
|
|
107
|
+
const base = block.lines.starts[line] - (block.flat ? line : 0);
|
|
108
|
+
const painted = [];
|
|
109
|
+
for (const token of block.lines.tokens[line] ?? []) {
|
|
110
|
+
const start = base + token.start;
|
|
111
|
+
const end = base + token.end;
|
|
112
|
+
const from = pointAt(block, start, start < end);
|
|
113
|
+
const to = pointAt(block, end, false);
|
|
114
|
+
if (from == null || to == null) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const highlight = highlightFor(token.type);
|
|
118
|
+
try {
|
|
119
|
+
const range = new Range();
|
|
120
|
+
range.setStart(...from);
|
|
121
|
+
range.setEnd(...to);
|
|
122
|
+
highlight.add(range);
|
|
123
|
+
painted.push({
|
|
124
|
+
range,
|
|
125
|
+
highlight
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
// A range the DOM refuses colors nothing.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
block.ranges[line] = painted;
|
|
132
|
+
}
|
|
133
|
+
function unpaintLine(block, line) {
|
|
134
|
+
for (const {
|
|
135
|
+
range,
|
|
136
|
+
highlight
|
|
137
|
+
} of block.ranges[line] ?? []) {
|
|
138
|
+
highlight.delete(range);
|
|
139
|
+
}
|
|
140
|
+
block.ranges[line] = [];
|
|
141
|
+
}
|
|
142
|
+
function unpaintAll(block) {
|
|
143
|
+
for (let line = 0; line < block.ranges.length; line++) {
|
|
144
|
+
unpaintLine(block, line);
|
|
145
|
+
}
|
|
146
|
+
block.ranges = [];
|
|
147
|
+
}
|
|
148
|
+
function repaintAll(block) {
|
|
149
|
+
unpaintAll(block);
|
|
150
|
+
for (let line = 0; line < block.lines.tokens.length; line++) {
|
|
151
|
+
paintLine(block, line);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Lists a block's DOM text nodes in order with their offsets, and the
|
|
157
|
+
* Lexical text nodes that render as exactly one of them.
|
|
158
|
+
*/
|
|
159
|
+
function $indexText(editor, node, block) {
|
|
160
|
+
const walker = document.createTreeWalker(block.element, NodeFilter.SHOW_TEXT);
|
|
161
|
+
while (walker.nextNode() != null) {
|
|
162
|
+
const text = walker.currentNode;
|
|
163
|
+
block.texts.push(text);
|
|
164
|
+
block.starts.push(block.length);
|
|
165
|
+
block.codeStarts.push(NaN);
|
|
166
|
+
block.length += text.length;
|
|
167
|
+
}
|
|
168
|
+
const indexes = new Map(block.texts.map((text, index) => [text, index]));
|
|
169
|
+
let codeStart = 0;
|
|
170
|
+
for (const child of node.getChildren()) {
|
|
171
|
+
const dom = $isTextNode(child) ? editor.getElementByKey(child.getKey()) : null;
|
|
172
|
+
const index = dom?.childNodes.length === 1 ? indexes.get(dom.firstChild) : undefined;
|
|
173
|
+
if (index != null) {
|
|
174
|
+
block.leaves.set(child.getKey(), {
|
|
175
|
+
index,
|
|
176
|
+
text: child.getTextContent()
|
|
177
|
+
});
|
|
178
|
+
block.codeStarts[index] = codeStart;
|
|
179
|
+
}
|
|
180
|
+
codeStart += child.getTextContentSize();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The key of the code block that holds the node `key`, if any. */
|
|
185
|
+
function $codeBlockOf(key) {
|
|
186
|
+
let node = $getNodeByKey(key)?.getParent() ?? null;
|
|
187
|
+
while (node != null && !$isCodeNode(node)) {
|
|
188
|
+
node = node.getParent();
|
|
189
|
+
}
|
|
190
|
+
return node?.getKey() ?? null;
|
|
191
|
+
}
|
|
192
|
+
export function CodeSyntaxPlugin({
|
|
193
|
+
codeLanguages
|
|
194
|
+
} = {}) {
|
|
195
|
+
const [editor] = useLexicalComposerContext();
|
|
196
|
+
// Keep one list while it names the same values, so an inline
|
|
197
|
+
// `codeLanguages={[…]}` does not re-colour every block on each parent
|
|
198
|
+
// render (adjusting state during render, not in an Effect).
|
|
199
|
+
const [languages, setLanguages] = useState(codeLanguages);
|
|
200
|
+
if (!sameLanguages(languages, codeLanguages)) {
|
|
201
|
+
setLanguages(codeLanguages);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Synchronizes the editor's code blocks with the CSS highlight registry:
|
|
205
|
+
// Lexical mutation and update listeners colour each block, and the cleanup
|
|
206
|
+
// removes the listeners and every highlight range they added.
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!supportsHighlights()) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
ensureRules();
|
|
212
|
+
// Every code block in the editor, the ones colored, and the ones colored
|
|
213
|
+
// in full during the current update.
|
|
214
|
+
const blocks = new Set();
|
|
215
|
+
const colored = new Map();
|
|
216
|
+
const repainted = new Set();
|
|
217
|
+
const uncolor = key => {
|
|
218
|
+
const block = colored.get(key);
|
|
219
|
+
if (block != null) {
|
|
220
|
+
unpaintAll(block);
|
|
221
|
+
colored.delete(key);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Colors a block in full, unless `force` is false and its element,
|
|
225
|
+
// language, and code are as last colored.
|
|
226
|
+
const $color = (key, force) => {
|
|
227
|
+
const node = $getNodeByKey(key);
|
|
228
|
+
const element = editor.getElementByKey(key);
|
|
229
|
+
if (!$isCodeNode(node) || element == null) {
|
|
230
|
+
uncolor(key);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const code = node.getTextContent();
|
|
234
|
+
const language = node.getLanguage() ?? '';
|
|
235
|
+
const previous = colored.get(key);
|
|
236
|
+
if (!force && previous != null && previous.element === element && previous.language === language && previous.lines.text === code) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
uncolor(key);
|
|
240
|
+
const rendered = element.textContent ?? '';
|
|
241
|
+
if (rendered !== code && rendered !== code.replaceAll('\n', '')) {
|
|
242
|
+
// Not rendered yet; the block's next change tries again.
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
ensureHighlightStyles();
|
|
246
|
+
const block = {
|
|
247
|
+
element,
|
|
248
|
+
language,
|
|
249
|
+
lines: new CodeLineTokens(code, listedLanguage(languages, language) ?? language),
|
|
250
|
+
flat: rendered !== code,
|
|
251
|
+
texts: [],
|
|
252
|
+
starts: [],
|
|
253
|
+
codeStarts: [],
|
|
254
|
+
length: 0,
|
|
255
|
+
leaves: new Map(),
|
|
256
|
+
ranges: []
|
|
257
|
+
};
|
|
258
|
+
$indexText(editor, node, block);
|
|
259
|
+
repaintAll(block);
|
|
260
|
+
colored.set(key, block);
|
|
261
|
+
repainted.add(key);
|
|
262
|
+
};
|
|
263
|
+
// Recolors a block after an update that rewrote the text of its leaf
|
|
264
|
+
// `leafKey` and changed nothing else in it. False when the block must
|
|
265
|
+
// be colored in full instead.
|
|
266
|
+
const $recolorLeaf = (key, leafKey) => {
|
|
267
|
+
const block = colored.get(key);
|
|
268
|
+
const leaf = block?.leaves.get(leafKey);
|
|
269
|
+
const node = $getNodeByKey(leafKey);
|
|
270
|
+
const parent = node?.getParent();
|
|
271
|
+
if (block == null || leaf == null || !$isTextNode(node) || !$isCodeNode(parent) || parent.getKey() !== key || (parent.getLanguage() ?? '') !== block.language || editor.getElementByKey(key) !== block.element) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const text = block.texts[leaf.index];
|
|
275
|
+
const dom = editor.getElementByKey(leafKey);
|
|
276
|
+
const after = node.getTextContent();
|
|
277
|
+
if (dom?.childNodes.length !== 1 || dom.firstChild !== text || text.data !== after) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const before = leaf.text;
|
|
281
|
+
if (after === before) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
let first = 0;
|
|
285
|
+
while (first < before.length && first < after.length && before[first] === after[first]) {
|
|
286
|
+
first++;
|
|
287
|
+
}
|
|
288
|
+
let beforeEnd = before.length;
|
|
289
|
+
let afterEnd = after.length;
|
|
290
|
+
while (beforeEnd > first && afterEnd > first && before[beforeEnd - 1] === after[afterEnd - 1]) {
|
|
291
|
+
beforeEnd--;
|
|
292
|
+
afterEnd--;
|
|
293
|
+
}
|
|
294
|
+
const inserted = after.slice(first, afterEnd);
|
|
295
|
+
// A flat block's text holds no line breaks; one typed in would no
|
|
296
|
+
// longer lay the code over its rendered text.
|
|
297
|
+
if (block.flat && inserted.includes('\n')) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const leafStart = block.codeStarts[leaf.index];
|
|
301
|
+
const lines = block.lines.replace(leafStart + first, beforeEnd - first, inserted);
|
|
302
|
+
const delta = after.length - before.length;
|
|
303
|
+
for (let i = leaf.index + 1; i < block.texts.length; i++) {
|
|
304
|
+
block.starts[i] += delta;
|
|
305
|
+
block.codeStarts[i] += delta;
|
|
306
|
+
}
|
|
307
|
+
block.length += delta;
|
|
308
|
+
leaf.text = after;
|
|
309
|
+
if (lines == null) {
|
|
310
|
+
repaintAll(block);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
// Lexical rewrites a changed DOM text node whole, which leaves every
|
|
314
|
+
// range in it stale: recolor each line the node holds, and any other
|
|
315
|
+
// line the edit re-tokenized. Every other range sits in a text node of
|
|
316
|
+
// its own line and is still right.
|
|
317
|
+
const recolor = new Set(lines);
|
|
318
|
+
const last = block.lines.lineAt(leafStart + Math.max(after.length - 1, 0));
|
|
319
|
+
for (let i = block.lines.lineAt(leafStart); i <= last; i++) {
|
|
320
|
+
recolor.add(i);
|
|
321
|
+
}
|
|
322
|
+
for (const i of recolor) {
|
|
323
|
+
unpaintLine(block, i);
|
|
324
|
+
paintLine(block, i);
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
const unregisterMutations = editor.registerMutationListener(CodeNode, mutations => {
|
|
329
|
+
editor.getEditorState().read(() => {
|
|
330
|
+
for (const [key, mutation] of mutations) {
|
|
331
|
+
if (mutation === 'destroyed') {
|
|
332
|
+
blocks.delete(key);
|
|
333
|
+
uncolor(key);
|
|
334
|
+
} else {
|
|
335
|
+
blocks.add(key);
|
|
336
|
+
$color(key, false);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}, {
|
|
341
|
+
skipInitialization: false
|
|
342
|
+
});
|
|
343
|
+
repainted.clear();
|
|
344
|
+
// Typing in a block changes its text nodes, not the block itself: after
|
|
345
|
+
// each update, recolor the blocks whose text nodes changed. A block whose
|
|
346
|
+
// only change is one leaf's text takes the partial path; any other
|
|
347
|
+
// change colors it in full, unless the block listener just did.
|
|
348
|
+
const unregisterUpdates = editor.registerUpdateListener(({
|
|
349
|
+
editorState,
|
|
350
|
+
mutatedNodes,
|
|
351
|
+
prevEditorState
|
|
352
|
+
}) => {
|
|
353
|
+
if (mutatedNodes != null && blocks.size > 0) {
|
|
354
|
+
const mutations = [];
|
|
355
|
+
for (const nodes of mutatedNodes.values()) {
|
|
356
|
+
for (const mutation of nodes) {
|
|
357
|
+
mutations.push(mutation);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const before = prevEditorState.read(() => mutations.map(([key, mutation]) => mutation === 'created' ? null : $codeBlockOf(key)));
|
|
361
|
+
editorState.read(() => {
|
|
362
|
+
// Each changed block, with the one leaf whose text an update
|
|
363
|
+
// rewrote, or null when anything else in it changed.
|
|
364
|
+
const changed = new Map();
|
|
365
|
+
const note = (key, leafKey) => {
|
|
366
|
+
if (key != null && blocks.has(key)) {
|
|
367
|
+
changed.set(key, changed.has(key) ? null : leafKey);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
mutations.forEach(([key, mutation], i) => {
|
|
371
|
+
const after = mutation === 'destroyed' ? null : $codeBlockOf(key);
|
|
372
|
+
const rewritten = mutation === 'updated' && after === before[i] && $isTextNode($getNodeByKey(key));
|
|
373
|
+
note(after, rewritten ? key : null);
|
|
374
|
+
if (before[i] !== after) {
|
|
375
|
+
note(before[i], null);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
for (const [key, leafKey] of changed) {
|
|
379
|
+
if (!repainted.has(key) && (leafKey == null || !$recolorLeaf(key, leafKey))) {
|
|
380
|
+
$color(key, true);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
repainted.clear();
|
|
386
|
+
});
|
|
387
|
+
return () => {
|
|
388
|
+
unregisterMutations();
|
|
389
|
+
unregisterUpdates();
|
|
390
|
+
for (const key of [...colored.keys()]) {
|
|
391
|
+
uncolor(key);
|
|
392
|
+
}
|
|
393
|
+
blocks.clear();
|
|
394
|
+
};
|
|
395
|
+
}, [editor, languages]);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeTabPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/CodeTabPlugin.tsx"],"names":[],"mappings":"AAwQA,wBAAgB,aAAa,IAAI,IAAI,CAiCpC"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file CodeTabPlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/code (CodeNode), and
|
|
8
|
+
* lexical.
|
|
9
|
+
* @output Exports CodeTabPlugin, which makes Tab and Shift+Tab in a code block
|
|
10
|
+
* indent and outdent every line the selection touches, by the indent the
|
|
11
|
+
* block's own lines use.
|
|
12
|
+
* @position Rendered by RichTextEditor; always on, and not exported from the
|
|
13
|
+
* package. A press indents by the block's own indent, so edited code keeps
|
|
14
|
+
* the indentation its author chose; the selection stays on the same
|
|
15
|
+
* characters, and each press is one undo step. It replaces only what Tab
|
|
16
|
+
* did in a code block before: TabIndentationPlugin put a tab character in
|
|
17
|
+
* place of the selection. Its listener sits at the front of the
|
|
18
|
+
* editor-priority queue, ahead of TabIndentationPlugin and behind every
|
|
19
|
+
* low-priority listener, so Escape then Tab still moves focus out of the
|
|
20
|
+
* editor (TabFocusEscapePlugin, WCAG 2.1.2). A selection that reaches
|
|
21
|
+
* outside the code block keeps TabIndentationPlugin's meaning. Read-only
|
|
22
|
+
* and disabled editors are unchanged.
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect } from 'react';
|
|
25
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
26
|
+
import { $isCodeNode } from '@lexical/code';
|
|
27
|
+
import { $addUpdateTag, $createTextNode, $getSelection, $isRangeSelection, $isTabNode, $isTextNode, COMMAND_PRIORITY_BEFORE_EDITOR, HISTORY_PUSH_TAG, KEY_TAB_COMMAND } from 'lexical';
|
|
28
|
+
|
|
29
|
+
/** The narrowest space indent a block uses, and the widest. */
|
|
30
|
+
const MIN_SPACE_INDENT = 2;
|
|
31
|
+
const MAX_SPACE_INDENT = 8;
|
|
32
|
+
function leadingSpaces(line) {
|
|
33
|
+
return /^ */.exec(line)?.[0].length ?? 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The indent a code block's lines use: a tab when any line starts with one;
|
|
38
|
+
* otherwise the fewest spaces an indented line starts with, kept between two
|
|
39
|
+
* and eight; otherwise two spaces.
|
|
40
|
+
*/
|
|
41
|
+
function codeIndentUnit(lines) {
|
|
42
|
+
if (lines.some(line => line.startsWith('\t'))) {
|
|
43
|
+
return '\t';
|
|
44
|
+
}
|
|
45
|
+
const widths = lines.map(leadingSpaces).filter(width => width > 0);
|
|
46
|
+
const width = widths.length === 0 ? MIN_SPACE_INDENT : Math.min(...widths);
|
|
47
|
+
return ' '.repeat(Math.min(MAX_SPACE_INDENT, Math.max(MIN_SPACE_INDENT, width)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A code block's children, the offset in the block's text where each one
|
|
52
|
+
* starts, and that text. A line ends at a line break node or at a newline
|
|
53
|
+
* inside a text node: imported code holds newlines, and Enter adds line
|
|
54
|
+
* break nodes.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
function $layoutOf(code) {
|
|
58
|
+
const children = code.getChildren();
|
|
59
|
+
const starts = [];
|
|
60
|
+
let text = '';
|
|
61
|
+
for (const child of children) {
|
|
62
|
+
starts.push(text.length);
|
|
63
|
+
text += child.getTextContent();
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
children,
|
|
67
|
+
starts,
|
|
68
|
+
text
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A text node that can take a caret and grow: any but a tab node. */
|
|
73
|
+
function $isPlainText(node) {
|
|
74
|
+
return $isTextNode(node) && !$isTabNode(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The code block that holds both ends of the selection, or null. */
|
|
78
|
+
function $selectionCodeBlock(selection) {
|
|
79
|
+
const blockOf = point => {
|
|
80
|
+
const node = point.getNode();
|
|
81
|
+
return $isCodeNode(node) ? node : node.getParent();
|
|
82
|
+
};
|
|
83
|
+
const block = blockOf(selection.anchor);
|
|
84
|
+
return $isCodeNode(block) && block.is(blockOf(selection.focus)) ? block : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where a point of the block's selection falls in the block's text. */
|
|
88
|
+
function $offsetOf(code, layout, point) {
|
|
89
|
+
if (point.key === code.getKey()) {
|
|
90
|
+
return point.offset < layout.children.length ? layout.starts[point.offset] : layout.text.length;
|
|
91
|
+
}
|
|
92
|
+
const index = layout.children.findIndex(child => child.getKey() === point.key);
|
|
93
|
+
return index < 0 ? 0 : layout.starts[index] + point.offset;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Moves a selection point to an offset in the block's text: into the text
|
|
98
|
+
* node there, or between children where none holds it (beside a tab or a
|
|
99
|
+
* line break, or on an empty line).
|
|
100
|
+
*/
|
|
101
|
+
function $placePoint(code, point, offset) {
|
|
102
|
+
const children = code.getChildren();
|
|
103
|
+
let start = 0;
|
|
104
|
+
for (let index = 0; index < children.length; index++) {
|
|
105
|
+
const child = children[index];
|
|
106
|
+
const size = child.getTextContentSize();
|
|
107
|
+
if ($isPlainText(child) && offset >= start && offset <= start + size) {
|
|
108
|
+
point.set(child.getKey(), offset - start, 'text');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (offset <= start) {
|
|
112
|
+
point.set(code.getKey(), index, 'element');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
start += size;
|
|
116
|
+
}
|
|
117
|
+
point.set(code.getKey(), children.length, 'element');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Inserts text at an offset in the block's text. */
|
|
121
|
+
function $insertAt(code, offset, text) {
|
|
122
|
+
const {
|
|
123
|
+
children,
|
|
124
|
+
starts
|
|
125
|
+
} = $layoutOf(code);
|
|
126
|
+
for (let index = 0; index < children.length; index++) {
|
|
127
|
+
const child = children[index];
|
|
128
|
+
const start = starts[index];
|
|
129
|
+
if ($isPlainText(child) && offset >= start && offset <= start + child.getTextContentSize()) {
|
|
130
|
+
const content = child.getTextContent();
|
|
131
|
+
const at = offset - start;
|
|
132
|
+
child.setTextContent(content.slice(0, at) + text + content.slice(at));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (offset <= start) {
|
|
136
|
+
child.insertBefore($createTextNode(text));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
code.append($createTextNode(text));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Removes `count` characters at an offset in the block's text. */
|
|
144
|
+
function $removeAt(code, offset, count) {
|
|
145
|
+
const {
|
|
146
|
+
children,
|
|
147
|
+
starts
|
|
148
|
+
} = $layoutOf(code);
|
|
149
|
+
let left = count;
|
|
150
|
+
for (let index = 0; index < children.length && left > 0; index++) {
|
|
151
|
+
const child = children[index];
|
|
152
|
+
const size = child.getTextContentSize();
|
|
153
|
+
if (!$isTextNode(child) || starts[index] + size <= offset) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const from = Math.max(0, offset - starts[index]);
|
|
157
|
+
const removed = Math.min(left, size - from);
|
|
158
|
+
const content = child.getTextContent();
|
|
159
|
+
const rest = content.slice(0, from) + content.slice(from + removed);
|
|
160
|
+
if (rest === '') {
|
|
161
|
+
child.remove();
|
|
162
|
+
} else {
|
|
163
|
+
child.setTextContent(rest);
|
|
164
|
+
}
|
|
165
|
+
left -= removed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Indents, or outdents, every line the selection touches by the block's own
|
|
171
|
+
* indent, and puts the selection back on the characters it held.
|
|
172
|
+
*/
|
|
173
|
+
function $shiftLines(code, selection, isOutdent) {
|
|
174
|
+
const layout = $layoutOf(code);
|
|
175
|
+
const lines = layout.text.split('\n');
|
|
176
|
+
const lineStarts = [];
|
|
177
|
+
let lineStart = 0;
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
lineStarts.push(lineStart);
|
|
180
|
+
lineStart += line.length + 1;
|
|
181
|
+
}
|
|
182
|
+
const lineAt = offset => {
|
|
183
|
+
let line = 0;
|
|
184
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= offset) {
|
|
185
|
+
line++;
|
|
186
|
+
}
|
|
187
|
+
return line;
|
|
188
|
+
};
|
|
189
|
+
const unit = codeIndentUnit(lines);
|
|
190
|
+
// A tab-indented block outdents a space-indented line by the narrowest
|
|
191
|
+
// space indent.
|
|
192
|
+
const spaceUnit = unit === '\t' ? MIN_SPACE_INDENT : unit.length;
|
|
193
|
+
const anchor = $offsetOf(code, layout, selection.anchor);
|
|
194
|
+
const focus = $offsetOf(code, layout, selection.focus);
|
|
195
|
+
const start = Math.min(anchor, focus);
|
|
196
|
+
const end = Math.max(anchor, focus);
|
|
197
|
+
const first = lineAt(start);
|
|
198
|
+
let last = lineAt(end);
|
|
199
|
+
// A selection that ends where a line begins does not reach into that line.
|
|
200
|
+
if (last > first && lineStarts[last] === end) {
|
|
201
|
+
last--;
|
|
202
|
+
}
|
|
203
|
+
// Each edit as [offset, characters added (negative when removed)], made
|
|
204
|
+
// from the last line up so the offsets of the lines above stay true.
|
|
205
|
+
const edits = [];
|
|
206
|
+
for (let line = last; line >= first; line--) {
|
|
207
|
+
const at = lineStarts[line];
|
|
208
|
+
if (!isOutdent) {
|
|
209
|
+
$insertAt(code, at, unit);
|
|
210
|
+
edits.push([at, unit.length]);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const removed = lines[line].startsWith('\t') ? 1 : Math.min(leadingSpaces(lines[line]), spaceUnit);
|
|
214
|
+
if (removed > 0) {
|
|
215
|
+
$removeAt(code, at, removed);
|
|
216
|
+
edits.push([at, -removed]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A point keeps its character: text added at or before it pushes it on,
|
|
220
|
+
// and a point inside removed indentation moves to its line's start.
|
|
221
|
+
const shifted = offset => edits.reduce((moved, [at, delta]) => {
|
|
222
|
+
if (delta > 0) {
|
|
223
|
+
return offset >= at ? moved + delta : moved;
|
|
224
|
+
}
|
|
225
|
+
return offset > at ? moved - Math.min(-delta, offset - at) : moved;
|
|
226
|
+
}, offset);
|
|
227
|
+
$placePoint(code, selection.anchor, shifted(anchor));
|
|
228
|
+
$placePoint(code, selection.focus, shifted(focus));
|
|
229
|
+
}
|
|
230
|
+
export function CodeTabPlugin() {
|
|
231
|
+
const [editor] = useLexicalComposerContext();
|
|
232
|
+
useEffect(() => editor.registerCommand(KEY_TAB_COMMAND, event => {
|
|
233
|
+
const selection = $getSelection();
|
|
234
|
+
if (!editor.isEditable() || !$isRangeSelection(selection)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const code = $selectionCodeBlock(selection);
|
|
238
|
+
if (code == null) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
// One undo step per press, even when a press adds or removes a
|
|
243
|
+
// single character beside the caret. The push tag starts a step
|
|
244
|
+
// before the press. A dirty block makes history class the press
|
|
245
|
+
// as its own change, so typing after it starts another step.
|
|
246
|
+
$addUpdateTag(HISTORY_PUSH_TAG);
|
|
247
|
+
$shiftLines(code, selection, event.shiftKey);
|
|
248
|
+
code.markDirty();
|
|
249
|
+
return true;
|
|
250
|
+
},
|
|
251
|
+
// After TabFocusEscapePlugin's low-priority listener, which lets an
|
|
252
|
+
// armed Tab move focus, and before TabIndentationPlugin's; see the
|
|
253
|
+
// file header.
|
|
254
|
+
COMMAND_PRIORITY_BEFORE_EDITOR), [editor]);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DisabledLinkGuardPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/DisabledLinkGuardPlugin.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,uBAAuB,CAAC,EAAC,UAAU,EAAC,EAAE;IAAC,UAAU,EAAE,OAAO,CAAA;CAAC,QAwB1E"}
|