@longd/layout-ui 0.1.0 → 0.1.2
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/README.md +257 -1
- package/dist/CATEditor-C-b6vybW.d.cts +381 -0
- package/dist/CATEditor-CLp6jZAf.d.ts +381 -0
- package/dist/chunk-BLJWR4ZV.js +11 -0
- package/dist/chunk-BLJWR4ZV.js.map +1 -0
- package/dist/{chunk-CZ3IMHZ6.js → chunk-H7SY4VJU.js} +7 -11
- package/dist/chunk-H7SY4VJU.js.map +1 -0
- package/dist/chunk-YXQGAND3.js +137 -0
- package/dist/chunk-YXQGAND3.js.map +1 -0
- package/dist/chunk-ZME2TTK5.js +2527 -0
- package/dist/chunk-ZME2TTK5.js.map +1 -0
- package/dist/index.cjs +2612 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +504 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -2
- package/dist/layout/cat-editor.cjs +2669 -0
- package/dist/layout/cat-editor.cjs.map +1 -0
- package/dist/layout/cat-editor.css +504 -0
- package/dist/layout/cat-editor.css.map +1 -0
- package/dist/layout/cat-editor.d.cts +28 -0
- package/dist/layout/cat-editor.d.ts +28 -0
- package/dist/layout/cat-editor.js +29 -0
- package/dist/layout/cat-editor.js.map +1 -0
- package/dist/layout/select.cjs +2 -1
- package/dist/layout/select.cjs.map +1 -1
- package/dist/layout/select.js +2 -1
- package/dist/utils/detect-quotes.cjs +162 -0
- package/dist/utils/detect-quotes.cjs.map +1 -0
- package/dist/utils/detect-quotes.d.cts +88 -0
- package/dist/utils/detect-quotes.d.ts +88 -0
- package/dist/utils/detect-quotes.js +9 -0
- package/dist/utils/detect-quotes.js.map +1 -0
- package/package.json +39 -3
- package/dist/chunk-CZ3IMHZ6.js.map +0 -1
|
@@ -0,0 +1,2527 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "./chunk-BLJWR4ZV.js";
|
|
4
|
+
import {
|
|
5
|
+
detectQuotes
|
|
6
|
+
} from "./chunk-YXQGAND3.js";
|
|
7
|
+
|
|
8
|
+
// src/layout/cat-editor/CATEditor.tsx
|
|
9
|
+
import {
|
|
10
|
+
forwardRef,
|
|
11
|
+
useCallback as useCallback3,
|
|
12
|
+
useEffect as useEffect3,
|
|
13
|
+
useImperativeHandle,
|
|
14
|
+
useMemo as useMemo2,
|
|
15
|
+
useRef as useRef4,
|
|
16
|
+
useState as useState2
|
|
17
|
+
} from "react";
|
|
18
|
+
import {
|
|
19
|
+
$createParagraphNode as $createParagraphNode2,
|
|
20
|
+
$createRangeSelection as $createRangeSelection2,
|
|
21
|
+
$createTextNode as $createTextNode3,
|
|
22
|
+
$getNodeByKey,
|
|
23
|
+
$getRoot as $getRoot3,
|
|
24
|
+
$setSelection as $setSelection2,
|
|
25
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
26
|
+
KEY_DOWN_COMMAND
|
|
27
|
+
} from "lexical";
|
|
28
|
+
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
29
|
+
import { useLexicalComposerContext as useLexicalComposerContext3 } from "@lexical/react/LexicalComposerContext";
|
|
30
|
+
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
|
31
|
+
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
32
|
+
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
33
|
+
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
|
|
34
|
+
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
|
|
35
|
+
|
|
36
|
+
// src/layout/cat-editor/constants.ts
|
|
37
|
+
var CODEPOINT_DISPLAY_MAP = {
|
|
38
|
+
0: "\u2400",
|
|
39
|
+
9: "\u21E5",
|
|
40
|
+
10: "\u21A9",
|
|
41
|
+
12: "\u240C",
|
|
42
|
+
13: "\u21B5",
|
|
43
|
+
160: "\u237D",
|
|
44
|
+
8194: "\u2423",
|
|
45
|
+
8195: "\u2423",
|
|
46
|
+
8201: "\xB7",
|
|
47
|
+
8202: "\xB7",
|
|
48
|
+
8203: "\u2205",
|
|
49
|
+
8204: "\u2298",
|
|
50
|
+
8205: "\u2295",
|
|
51
|
+
8288: "\u2040",
|
|
52
|
+
12288: "\u25A1",
|
|
53
|
+
65279: "\u25CA"
|
|
54
|
+
};
|
|
55
|
+
var _codepointOverrides;
|
|
56
|
+
function setCodepointOverrides(overrides) {
|
|
57
|
+
_codepointOverrides = overrides;
|
|
58
|
+
}
|
|
59
|
+
function getEffectiveCodepointMap() {
|
|
60
|
+
return _codepointOverrides ? { ...CODEPOINT_DISPLAY_MAP, ..._codepointOverrides } : CODEPOINT_DISPLAY_MAP;
|
|
61
|
+
}
|
|
62
|
+
var NL_MARKER_PREFIX = "__nl-";
|
|
63
|
+
function replaceInvisibleChars(text, overrides) {
|
|
64
|
+
const map = overrides ? { ...CODEPOINT_DISPLAY_MAP, ...overrides } : getEffectiveCodepointMap();
|
|
65
|
+
let result = "";
|
|
66
|
+
for (const ch of text) {
|
|
67
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
68
|
+
result += map[cp] ?? ch;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/layout/cat-editor/highlight-node.ts
|
|
74
|
+
import { TextNode } from "lexical";
|
|
75
|
+
var HighlightNode = class _HighlightNode extends TextNode {
|
|
76
|
+
__highlightTypes;
|
|
77
|
+
__ruleIds;
|
|
78
|
+
__displayText;
|
|
79
|
+
static getType() {
|
|
80
|
+
return "highlight";
|
|
81
|
+
}
|
|
82
|
+
static clone(node) {
|
|
83
|
+
return new _HighlightNode(
|
|
84
|
+
node.__text,
|
|
85
|
+
node.__highlightTypes,
|
|
86
|
+
node.__ruleIds,
|
|
87
|
+
node.__displayText,
|
|
88
|
+
node.__key
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
constructor(text, highlightTypes, ruleIds, displayText, key) {
|
|
92
|
+
super(text, key);
|
|
93
|
+
this.__highlightTypes = highlightTypes;
|
|
94
|
+
this.__ruleIds = ruleIds;
|
|
95
|
+
this.__displayText = displayText ?? "";
|
|
96
|
+
}
|
|
97
|
+
createDOM(config) {
|
|
98
|
+
const dom = super.createDOM(config);
|
|
99
|
+
dom.classList.add("cat-highlight");
|
|
100
|
+
for (const t of this.__highlightTypes.split(",")) {
|
|
101
|
+
dom.classList.add(`cat-highlight-${t}`);
|
|
102
|
+
if (t.startsWith("glossary-")) {
|
|
103
|
+
dom.classList.add("cat-highlight-glossary");
|
|
104
|
+
}
|
|
105
|
+
if (t.startsWith("spellcheck-")) {
|
|
106
|
+
dom.classList.add("cat-highlight-spellcheck");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (this.__highlightTypes.includes(",")) {
|
|
110
|
+
dom.classList.add("cat-highlight-nested");
|
|
111
|
+
}
|
|
112
|
+
dom.dataset.highlightTypes = this.__highlightTypes;
|
|
113
|
+
dom.dataset.ruleIds = this.__ruleIds;
|
|
114
|
+
if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
|
|
115
|
+
dom.style.userSelect = "none";
|
|
116
|
+
dom.classList.add("cat-highlight-nl-marker");
|
|
117
|
+
}
|
|
118
|
+
if (this.__displayText) {
|
|
119
|
+
dom.dataset.display = this.__displayText;
|
|
120
|
+
}
|
|
121
|
+
if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
|
|
122
|
+
dom.textContent = "\u200B";
|
|
123
|
+
dom.contentEditable = "false";
|
|
124
|
+
}
|
|
125
|
+
if (this.__highlightTypes.split(",").includes("special-char")) {
|
|
126
|
+
if (this.__text === " ") {
|
|
127
|
+
dom.classList.add("cat-highlight-space-char");
|
|
128
|
+
dom.style.position = "relative";
|
|
129
|
+
} else {
|
|
130
|
+
const replaced = replaceInvisibleChars(this.__text);
|
|
131
|
+
if (replaced !== this.__text) {
|
|
132
|
+
dom.textContent = replaced;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
|
|
137
|
+
dom.classList.add("cat-highlight-quote-char");
|
|
138
|
+
dom.textContent = "\u200B";
|
|
139
|
+
dom.contentEditable = "false";
|
|
140
|
+
}
|
|
141
|
+
return dom;
|
|
142
|
+
}
|
|
143
|
+
updateDOM(prevNode, dom, config) {
|
|
144
|
+
const updated = super.updateDOM(prevNode, dom, config);
|
|
145
|
+
if (prevNode.__highlightTypes !== this.__highlightTypes) {
|
|
146
|
+
for (const t of prevNode.__highlightTypes.split(",")) {
|
|
147
|
+
dom.classList.remove(`cat-highlight-${t}`);
|
|
148
|
+
if (t.startsWith("glossary-")) {
|
|
149
|
+
dom.classList.remove("cat-highlight-glossary");
|
|
150
|
+
}
|
|
151
|
+
if (t.startsWith("spellcheck-")) {
|
|
152
|
+
dom.classList.remove("cat-highlight-spellcheck");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
dom.classList.remove("cat-highlight-nested");
|
|
156
|
+
for (const t of this.__highlightTypes.split(",")) {
|
|
157
|
+
dom.classList.add(`cat-highlight-${t}`);
|
|
158
|
+
if (t.startsWith("glossary-")) {
|
|
159
|
+
dom.classList.add("cat-highlight-glossary");
|
|
160
|
+
}
|
|
161
|
+
if (t.startsWith("spellcheck-")) {
|
|
162
|
+
dom.classList.add("cat-highlight-spellcheck");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (this.__highlightTypes.includes(",")) {
|
|
166
|
+
dom.classList.add("cat-highlight-nested");
|
|
167
|
+
}
|
|
168
|
+
dom.dataset.highlightTypes = this.__highlightTypes;
|
|
169
|
+
}
|
|
170
|
+
if (prevNode.__ruleIds !== this.__ruleIds) {
|
|
171
|
+
dom.dataset.ruleIds = this.__ruleIds;
|
|
172
|
+
}
|
|
173
|
+
if (prevNode.__displayText !== this.__displayText) {
|
|
174
|
+
if (this.__displayText) {
|
|
175
|
+
dom.dataset.display = this.__displayText;
|
|
176
|
+
} else {
|
|
177
|
+
delete dom.dataset.display;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
|
|
181
|
+
dom.textContent = "\u200B";
|
|
182
|
+
dom.contentEditable = "false";
|
|
183
|
+
} else if (dom.contentEditable === "false") {
|
|
184
|
+
dom.removeAttribute("contenteditable");
|
|
185
|
+
}
|
|
186
|
+
if (this.__highlightTypes.split(",").includes("special-char")) {
|
|
187
|
+
if (this.__text === " ") {
|
|
188
|
+
dom.classList.add("cat-highlight-space-char");
|
|
189
|
+
dom.style.position = "relative";
|
|
190
|
+
} else {
|
|
191
|
+
const replaced = replaceInvisibleChars(this.__text);
|
|
192
|
+
if (replaced !== this.__text) {
|
|
193
|
+
dom.textContent = replaced;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
|
|
198
|
+
dom.classList.add("cat-highlight-quote-char");
|
|
199
|
+
dom.textContent = "\u200B";
|
|
200
|
+
dom.contentEditable = "false";
|
|
201
|
+
} else if (prevNode.__highlightTypes.split(",").includes("quote")) {
|
|
202
|
+
dom.classList.remove("cat-highlight-quote-char");
|
|
203
|
+
if (dom.contentEditable === "false") {
|
|
204
|
+
dom.removeAttribute("contenteditable");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return updated;
|
|
208
|
+
}
|
|
209
|
+
static importJSON(json) {
|
|
210
|
+
const node = new _HighlightNode(
|
|
211
|
+
json.text,
|
|
212
|
+
json.highlightTypes,
|
|
213
|
+
json.ruleIds,
|
|
214
|
+
json.displayText
|
|
215
|
+
);
|
|
216
|
+
node.setFormat(json.format);
|
|
217
|
+
node.setDetail(json.detail);
|
|
218
|
+
node.setMode(json.mode);
|
|
219
|
+
node.setStyle(json.style);
|
|
220
|
+
return node;
|
|
221
|
+
}
|
|
222
|
+
exportJSON() {
|
|
223
|
+
return {
|
|
224
|
+
...super.exportJSON(),
|
|
225
|
+
type: "highlight",
|
|
226
|
+
highlightTypes: this.__highlightTypes,
|
|
227
|
+
ruleIds: this.__ruleIds,
|
|
228
|
+
displayText: this.__displayText
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** NL-marker nodes must not leak the display symbol ↩ into
|
|
232
|
+
* clipboard or getTextContent() calls — they are purely visual. */
|
|
233
|
+
getTextContent() {
|
|
234
|
+
if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return "";
|
|
235
|
+
return super.getTextContent();
|
|
236
|
+
}
|
|
237
|
+
canInsertTextBefore() {
|
|
238
|
+
if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
|
|
239
|
+
if (this.getMode() === "token") return false;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
canInsertTextAfter() {
|
|
243
|
+
if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
|
|
244
|
+
if (this.getMode() === "token") return false;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
isTextEntity() {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
function $createHighlightNode(text, highlightTypes, ruleIds, displayText, forceToken) {
|
|
252
|
+
const node = new HighlightNode(text, highlightTypes, ruleIds, displayText);
|
|
253
|
+
if (highlightTypes.split(",").includes("special-char") || ruleIds.startsWith(NL_MARKER_PREFIX) || forceToken) {
|
|
254
|
+
node.setMode("token");
|
|
255
|
+
}
|
|
256
|
+
return node;
|
|
257
|
+
}
|
|
258
|
+
function $isHighlightNode(node) {
|
|
259
|
+
return node instanceof HighlightNode;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/layout/cat-editor/mention-node.ts
|
|
263
|
+
import { $applyNodeReplacement, TextNode as TextNode2 } from "lexical";
|
|
264
|
+
var DEFAULT_MENTION_SERIALIZE = (id) => `@{${id}}`;
|
|
265
|
+
var DEFAULT_MENTION_PATTERN = /@\{([^}]+)\}/g;
|
|
266
|
+
var _mentionNodeConfig = {};
|
|
267
|
+
function setMentionNodeConfig(config) {
|
|
268
|
+
_mentionNodeConfig = config;
|
|
269
|
+
}
|
|
270
|
+
function getMentionModelText(id) {
|
|
271
|
+
return (_mentionNodeConfig.serialize ?? DEFAULT_MENTION_SERIALIZE)(id);
|
|
272
|
+
}
|
|
273
|
+
function getMentionPattern() {
|
|
274
|
+
const src = _mentionNodeConfig.pattern ?? DEFAULT_MENTION_PATTERN;
|
|
275
|
+
return new RegExp(src.source, src.flags);
|
|
276
|
+
}
|
|
277
|
+
function renderDefaultMentionDOM(element, _mentionId, mentionName) {
|
|
278
|
+
element.textContent = "";
|
|
279
|
+
const label = document.createElement("span");
|
|
280
|
+
label.className = "cat-mention-label";
|
|
281
|
+
label.textContent = `@${mentionName}`;
|
|
282
|
+
element.appendChild(label);
|
|
283
|
+
}
|
|
284
|
+
function $convertMentionElement(domNode) {
|
|
285
|
+
const mentionId = domNode.getAttribute("data-mention-id");
|
|
286
|
+
const mentionName = domNode.getAttribute("data-mention-name");
|
|
287
|
+
if (mentionId !== null) {
|
|
288
|
+
const node = $createMentionNode(mentionId, mentionName ?? mentionId);
|
|
289
|
+
return { node };
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
var MentionNode = class _MentionNode extends TextNode2 {
|
|
294
|
+
__mentionId;
|
|
295
|
+
__mentionName;
|
|
296
|
+
static getType() {
|
|
297
|
+
return "mention";
|
|
298
|
+
}
|
|
299
|
+
static clone(node) {
|
|
300
|
+
return new _MentionNode(
|
|
301
|
+
node.__mentionId,
|
|
302
|
+
node.__mentionName,
|
|
303
|
+
node.__text,
|
|
304
|
+
node.__key
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
static importJSON(serializedNode) {
|
|
308
|
+
return $createMentionNode(
|
|
309
|
+
serializedNode.mentionId,
|
|
310
|
+
serializedNode.mentionName
|
|
311
|
+
).updateFromJSON(serializedNode);
|
|
312
|
+
}
|
|
313
|
+
constructor(mentionId, mentionName, text, key) {
|
|
314
|
+
super(text ?? getMentionModelText(mentionId), key);
|
|
315
|
+
this.__mentionId = mentionId;
|
|
316
|
+
this.__mentionName = mentionName;
|
|
317
|
+
}
|
|
318
|
+
exportJSON() {
|
|
319
|
+
return {
|
|
320
|
+
...super.exportJSON(),
|
|
321
|
+
mentionId: this.__mentionId,
|
|
322
|
+
mentionName: this.__mentionName
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
createDOM(config) {
|
|
326
|
+
const dom = super.createDOM(config);
|
|
327
|
+
dom.className = "cat-mention-node";
|
|
328
|
+
dom.spellcheck = false;
|
|
329
|
+
dom.contentEditable = "false";
|
|
330
|
+
dom.setAttribute("data-mention-id", this.__mentionId);
|
|
331
|
+
dom.setAttribute("data-mention-name", this.__mentionName);
|
|
332
|
+
this._renderInnerDOM(dom);
|
|
333
|
+
return dom;
|
|
334
|
+
}
|
|
335
|
+
updateDOM(prevNode, dom, _config) {
|
|
336
|
+
if (prevNode.__mentionId !== this.__mentionId || prevNode.__mentionName !== this.__mentionName) {
|
|
337
|
+
dom.setAttribute("data-mention-id", this.__mentionId);
|
|
338
|
+
dom.setAttribute("data-mention-name", this.__mentionName);
|
|
339
|
+
this._renderInnerDOM(dom);
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
/** Fill the span with visible content (default: @label, or custom). */
|
|
344
|
+
_renderInnerDOM(element) {
|
|
345
|
+
const renderer = _mentionNodeConfig.renderDOM;
|
|
346
|
+
if (renderer) {
|
|
347
|
+
const handled = renderer(element, this.__mentionId, this.__mentionName);
|
|
348
|
+
if (handled) return;
|
|
349
|
+
}
|
|
350
|
+
renderDefaultMentionDOM(element, this.__mentionId, this.__mentionName);
|
|
351
|
+
}
|
|
352
|
+
exportDOM() {
|
|
353
|
+
const element = document.createElement("span");
|
|
354
|
+
element.setAttribute("data-mention", "true");
|
|
355
|
+
element.setAttribute("data-mention-id", this.__mentionId);
|
|
356
|
+
element.setAttribute("data-mention-name", this.__mentionName);
|
|
357
|
+
element.textContent = this.__text;
|
|
358
|
+
return { element };
|
|
359
|
+
}
|
|
360
|
+
static importDOM() {
|
|
361
|
+
return {
|
|
362
|
+
span: (domNode) => {
|
|
363
|
+
if (!domNode.hasAttribute("data-mention")) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
conversion: $convertMentionElement,
|
|
368
|
+
priority: 1
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
isTextEntity() {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
canInsertTextBefore() {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
canInsertTextAfter() {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function $createMentionNode(mentionId, mentionName, textContent) {
|
|
384
|
+
const node = new MentionNode(
|
|
385
|
+
mentionId,
|
|
386
|
+
mentionName,
|
|
387
|
+
textContent ?? getMentionModelText(mentionId)
|
|
388
|
+
);
|
|
389
|
+
node.setMode("token").toggleDirectionless();
|
|
390
|
+
return $applyNodeReplacement(node);
|
|
391
|
+
}
|
|
392
|
+
function $isMentionNode(node) {
|
|
393
|
+
return node instanceof MentionNode;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/layout/cat-editor/mention-plugin.tsx
|
|
397
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
398
|
+
import * as ReactDOM from "react-dom";
|
|
399
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
400
|
+
import {
|
|
401
|
+
LexicalTypeaheadMenuPlugin,
|
|
402
|
+
MenuOption,
|
|
403
|
+
useBasicTypeaheadTriggerMatch
|
|
404
|
+
} from "@lexical/react/LexicalTypeaheadMenuPlugin";
|
|
405
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
406
|
+
import { $createTextNode } from "lexical";
|
|
407
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
408
|
+
var SUGGESTION_LIST_LENGTH_LIMIT = 50;
|
|
409
|
+
var ITEM_HEIGHT = 36;
|
|
410
|
+
var MentionTypeaheadOption = class extends MenuOption {
|
|
411
|
+
user;
|
|
412
|
+
constructor(user) {
|
|
413
|
+
super(user.id);
|
|
414
|
+
this.user = user;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function MentionAvatar({
|
|
418
|
+
user,
|
|
419
|
+
size = 24
|
|
420
|
+
}) {
|
|
421
|
+
if (user.avatar) {
|
|
422
|
+
return /* @__PURE__ */ jsx(
|
|
423
|
+
"span",
|
|
424
|
+
{
|
|
425
|
+
className: "inline-flex items-center justify-center",
|
|
426
|
+
style: { width: size, height: size },
|
|
427
|
+
children: user.avatar()
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const initials = user.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
|
|
432
|
+
return /* @__PURE__ */ jsx(
|
|
433
|
+
"span",
|
|
434
|
+
{
|
|
435
|
+
className: "inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium",
|
|
436
|
+
style: { width: size, height: size, fontSize: size * 0.4 },
|
|
437
|
+
children: initials
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
function MentionMenuItem({
|
|
442
|
+
option,
|
|
443
|
+
isSelected,
|
|
444
|
+
onClick,
|
|
445
|
+
onMouseEnter
|
|
446
|
+
}) {
|
|
447
|
+
return /* @__PURE__ */ jsxs(
|
|
448
|
+
"li",
|
|
449
|
+
{
|
|
450
|
+
tabIndex: -1,
|
|
451
|
+
className: `flex items-center gap-2 px-2 py-1.5 text-sm cursor-default select-none rounded-sm ${isSelected ? "bg-accent text-accent-foreground" : "text-popover-foreground"}`,
|
|
452
|
+
ref: option.setRefElement,
|
|
453
|
+
role: "option",
|
|
454
|
+
"aria-selected": isSelected,
|
|
455
|
+
onMouseEnter,
|
|
456
|
+
onClick,
|
|
457
|
+
children: [
|
|
458
|
+
/* @__PURE__ */ jsx(MentionAvatar, { user: option.user, size: 22 }),
|
|
459
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: option.user.name })
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
option.key
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function VirtualizedMentionMenu({
|
|
466
|
+
options,
|
|
467
|
+
selectedIndex,
|
|
468
|
+
selectOptionAndCleanUp,
|
|
469
|
+
setHighlightedIndex
|
|
470
|
+
}) {
|
|
471
|
+
const parentRef = useRef(null);
|
|
472
|
+
const virtualizer = useVirtualizer({
|
|
473
|
+
count: options.length,
|
|
474
|
+
getScrollElement: () => parentRef.current,
|
|
475
|
+
estimateSize: () => ITEM_HEIGHT,
|
|
476
|
+
overscan: 8
|
|
477
|
+
});
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (selectedIndex !== null && selectedIndex >= 0) {
|
|
480
|
+
virtualizer.scrollToIndex(selectedIndex, { align: "auto" });
|
|
481
|
+
}
|
|
482
|
+
}, [selectedIndex, virtualizer]);
|
|
483
|
+
return /* @__PURE__ */ jsx(
|
|
484
|
+
"div",
|
|
485
|
+
{
|
|
486
|
+
ref: parentRef,
|
|
487
|
+
className: "max-h-[280px] overflow-y-auto overflow-x-hidden",
|
|
488
|
+
children: /* @__PURE__ */ jsx(
|
|
489
|
+
"ul",
|
|
490
|
+
{
|
|
491
|
+
role: "listbox",
|
|
492
|
+
"aria-label": "Mention suggestions",
|
|
493
|
+
style: {
|
|
494
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
495
|
+
position: "relative",
|
|
496
|
+
width: "100%"
|
|
497
|
+
},
|
|
498
|
+
children: virtualizer.getVirtualItems().map((vItem) => {
|
|
499
|
+
const option = options[vItem.index];
|
|
500
|
+
return /* @__PURE__ */ jsx(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
style: {
|
|
504
|
+
position: "absolute",
|
|
505
|
+
top: 0,
|
|
506
|
+
left: 0,
|
|
507
|
+
width: "100%",
|
|
508
|
+
transform: `translateY(${vItem.start}px)`
|
|
509
|
+
},
|
|
510
|
+
ref: virtualizer.measureElement,
|
|
511
|
+
"data-index": vItem.index,
|
|
512
|
+
children: /* @__PURE__ */ jsx(
|
|
513
|
+
MentionMenuItem,
|
|
514
|
+
{
|
|
515
|
+
option,
|
|
516
|
+
isSelected: selectedIndex === vItem.index,
|
|
517
|
+
onClick: () => {
|
|
518
|
+
setHighlightedIndex(vItem.index);
|
|
519
|
+
selectOptionAndCleanUp(option);
|
|
520
|
+
},
|
|
521
|
+
onMouseEnter: () => {
|
|
522
|
+
setHighlightedIndex(vItem.index);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
},
|
|
527
|
+
option.key
|
|
528
|
+
);
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
function checkForMentionMatch(text, trigger) {
|
|
536
|
+
const escaped = trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
537
|
+
const regex = new RegExp(
|
|
538
|
+
`(^|\\s|\\()([${escaped}]((?:[^${escaped}\\s]){0,75}))$`
|
|
539
|
+
);
|
|
540
|
+
const match = regex.exec(text);
|
|
541
|
+
if (match !== null) {
|
|
542
|
+
const maybeLeadingWhitespace = match[1];
|
|
543
|
+
const matchingString = match[3];
|
|
544
|
+
return {
|
|
545
|
+
leadOffset: match.index + maybeLeadingWhitespace.length,
|
|
546
|
+
matchingString,
|
|
547
|
+
replaceableString: match[2]
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
function MentionPlugin({
|
|
553
|
+
users,
|
|
554
|
+
trigger = "@",
|
|
555
|
+
onMentionInsert
|
|
556
|
+
}) {
|
|
557
|
+
const [editor] = useLexicalComposerContext();
|
|
558
|
+
const [queryString, setQueryString] = useState(null);
|
|
559
|
+
const checkForSlashTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
|
|
560
|
+
minLength: 0
|
|
561
|
+
});
|
|
562
|
+
const results = useMemo(() => {
|
|
563
|
+
if (queryString === null)
|
|
564
|
+
return users.slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
|
|
565
|
+
const q = queryString.toLowerCase();
|
|
566
|
+
return users.filter((u) => u.name.toLowerCase().includes(q)).slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
|
|
567
|
+
}, [users, queryString]);
|
|
568
|
+
const options = useMemo(
|
|
569
|
+
() => results.map((user) => new MentionTypeaheadOption(user)),
|
|
570
|
+
[results]
|
|
571
|
+
);
|
|
572
|
+
const onSelectOption = useCallback(
|
|
573
|
+
(selectedOption, nodeToReplace, closeMenu) => {
|
|
574
|
+
editor.update(() => {
|
|
575
|
+
const mentionNode = $createMentionNode(
|
|
576
|
+
selectedOption.user.id,
|
|
577
|
+
selectedOption.user.name
|
|
578
|
+
);
|
|
579
|
+
if (nodeToReplace) {
|
|
580
|
+
nodeToReplace.replace(mentionNode);
|
|
581
|
+
}
|
|
582
|
+
const spaceNode = $createTextNode(" ");
|
|
583
|
+
mentionNode.insertAfter(spaceNode);
|
|
584
|
+
spaceNode.selectEnd();
|
|
585
|
+
closeMenu();
|
|
586
|
+
});
|
|
587
|
+
onMentionInsert?.(selectedOption.user);
|
|
588
|
+
},
|
|
589
|
+
[editor, onMentionInsert]
|
|
590
|
+
);
|
|
591
|
+
const checkForMentionTrigger = useCallback(
|
|
592
|
+
(text) => {
|
|
593
|
+
const slashMatch = checkForSlashTriggerMatch(text, editor);
|
|
594
|
+
if (slashMatch !== null) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return checkForMentionMatch(text, trigger);
|
|
598
|
+
},
|
|
599
|
+
[checkForSlashTriggerMatch, editor, trigger]
|
|
600
|
+
);
|
|
601
|
+
return /* @__PURE__ */ jsx(
|
|
602
|
+
LexicalTypeaheadMenuPlugin,
|
|
603
|
+
{
|
|
604
|
+
onQueryChange: setQueryString,
|
|
605
|
+
onSelectOption,
|
|
606
|
+
triggerFn: checkForMentionTrigger,
|
|
607
|
+
options,
|
|
608
|
+
menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => anchorElementRef.current && options.length ? ReactDOM.createPortal(
|
|
609
|
+
/* @__PURE__ */ jsx("div", { className: "cat-mention-popover", children: /* @__PURE__ */ jsx(
|
|
610
|
+
VirtualizedMentionMenu,
|
|
611
|
+
{
|
|
612
|
+
options,
|
|
613
|
+
selectedIndex,
|
|
614
|
+
selectOptionAndCleanUp,
|
|
615
|
+
setHighlightedIndex
|
|
616
|
+
}
|
|
617
|
+
) }),
|
|
618
|
+
anchorElementRef.current
|
|
619
|
+
) : null
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/layout/cat-editor/plugins.tsx
|
|
625
|
+
import { useLexicalComposerContext as useLexicalComposerContext2 } from "@lexical/react/LexicalComposerContext";
|
|
626
|
+
import { $isParentElementRTL } from "@lexical/selection";
|
|
627
|
+
import {
|
|
628
|
+
$addUpdateTag,
|
|
629
|
+
$createParagraphNode,
|
|
630
|
+
$createRangeSelection,
|
|
631
|
+
$createTextNode as $createTextNode2,
|
|
632
|
+
$getRoot as $getRoot2,
|
|
633
|
+
$getSelection,
|
|
634
|
+
$isRangeSelection,
|
|
635
|
+
$setSelection,
|
|
636
|
+
COMMAND_PRIORITY_HIGH,
|
|
637
|
+
KEY_ARROW_LEFT_COMMAND,
|
|
638
|
+
KEY_ARROW_RIGHT_COMMAND,
|
|
639
|
+
PASTE_COMMAND,
|
|
640
|
+
SELECTION_CHANGE_COMMAND
|
|
641
|
+
} from "lexical";
|
|
642
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
643
|
+
|
|
644
|
+
// src/layout/cat-editor/compute-segments.ts
|
|
645
|
+
var TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>/g;
|
|
646
|
+
function detectAndPairTags(text, _detectInner = true) {
|
|
647
|
+
const allTags = [];
|
|
648
|
+
TAG_RE.lastIndex = 0;
|
|
649
|
+
let m;
|
|
650
|
+
while ((m = TAG_RE.exec(text)) !== null) {
|
|
651
|
+
const isClosing = m[1] === "/";
|
|
652
|
+
const isSelfClosing = m[3] === "/" || !isClosing && m[0].endsWith("/>");
|
|
653
|
+
allTags.push({
|
|
654
|
+
start: m.index,
|
|
655
|
+
end: m.index + m[0].length,
|
|
656
|
+
tagName: m[2].toLowerCase(),
|
|
657
|
+
isClosing,
|
|
658
|
+
isSelfClosing,
|
|
659
|
+
originalText: m[0]
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
let nextNum = 1;
|
|
663
|
+
const stack = [];
|
|
664
|
+
const result = [];
|
|
665
|
+
for (let i = 0; i < allTags.length; i++) {
|
|
666
|
+
const tag = allTags[i];
|
|
667
|
+
if (tag.isSelfClosing) {
|
|
668
|
+
const num = nextNum++;
|
|
669
|
+
result.push({
|
|
670
|
+
...tag,
|
|
671
|
+
tagNumber: num,
|
|
672
|
+
displayText: `<${num}/>`
|
|
673
|
+
});
|
|
674
|
+
} else if (!tag.isClosing) {
|
|
675
|
+
const num = nextNum++;
|
|
676
|
+
stack.push({ name: tag.tagName, num, idx: i });
|
|
677
|
+
} else {
|
|
678
|
+
let matchIdx = -1;
|
|
679
|
+
for (let j = stack.length - 1; j >= 0; j--) {
|
|
680
|
+
if (stack[j].name === tag.tagName) {
|
|
681
|
+
matchIdx = j;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (matchIdx >= 0) {
|
|
686
|
+
const openEntry = stack[matchIdx];
|
|
687
|
+
stack.splice(matchIdx, 1);
|
|
688
|
+
const openTag = allTags[openEntry.idx];
|
|
689
|
+
result.push({
|
|
690
|
+
...openTag,
|
|
691
|
+
tagNumber: openEntry.num,
|
|
692
|
+
displayText: `<${openEntry.num}>`
|
|
693
|
+
});
|
|
694
|
+
result.push({
|
|
695
|
+
...tag,
|
|
696
|
+
tagNumber: openEntry.num,
|
|
697
|
+
displayText: `</${openEntry.num}>`
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
result.sort((a, b) => a.start - b.start);
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
var HTML_TAG_CLASSIFY = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>$/;
|
|
706
|
+
function detectCustomTags(text, patternSource) {
|
|
707
|
+
let re;
|
|
708
|
+
try {
|
|
709
|
+
re = new RegExp(patternSource, "g");
|
|
710
|
+
} catch {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
const matches = [];
|
|
714
|
+
let m;
|
|
715
|
+
while ((m = re.exec(text)) !== null) {
|
|
716
|
+
if (m[0].length === 0) {
|
|
717
|
+
re.lastIndex++;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const htmlMatch = HTML_TAG_CLASSIFY.exec(m[0]);
|
|
721
|
+
if (htmlMatch) {
|
|
722
|
+
matches.push({
|
|
723
|
+
start: m.index,
|
|
724
|
+
end: m.index + m[0].length,
|
|
725
|
+
text: m[0],
|
|
726
|
+
htmlName: htmlMatch[2].toLowerCase(),
|
|
727
|
+
isClosing: htmlMatch[1] === "/",
|
|
728
|
+
isSelfClosing: htmlMatch[3] === "/" || !htmlMatch[1] && m[0].endsWith("/>")
|
|
729
|
+
});
|
|
730
|
+
} else {
|
|
731
|
+
matches.push({
|
|
732
|
+
start: m.index,
|
|
733
|
+
end: m.index + m[0].length,
|
|
734
|
+
text: m[0],
|
|
735
|
+
htmlName: null,
|
|
736
|
+
isClosing: false,
|
|
737
|
+
isSelfClosing: false
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
let nextNum = 1;
|
|
742
|
+
const stack = [];
|
|
743
|
+
const result = [];
|
|
744
|
+
for (let i = 0; i < matches.length; i++) {
|
|
745
|
+
const raw = matches[i];
|
|
746
|
+
if (!raw.htmlName) {
|
|
747
|
+
const num = nextNum++;
|
|
748
|
+
result.push({
|
|
749
|
+
start: raw.start,
|
|
750
|
+
end: raw.end,
|
|
751
|
+
tagName: raw.text,
|
|
752
|
+
tagNumber: num,
|
|
753
|
+
isClosing: false,
|
|
754
|
+
isSelfClosing: false,
|
|
755
|
+
originalText: raw.text,
|
|
756
|
+
displayText: `<${num}>`
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (raw.isSelfClosing) {
|
|
761
|
+
const num = nextNum++;
|
|
762
|
+
result.push({
|
|
763
|
+
start: raw.start,
|
|
764
|
+
end: raw.end,
|
|
765
|
+
tagName: raw.htmlName,
|
|
766
|
+
tagNumber: num,
|
|
767
|
+
isClosing: false,
|
|
768
|
+
isSelfClosing: true,
|
|
769
|
+
originalText: raw.text,
|
|
770
|
+
displayText: `<${num}/>`
|
|
771
|
+
});
|
|
772
|
+
} else if (!raw.isClosing) {
|
|
773
|
+
const num = nextNum++;
|
|
774
|
+
stack.push({ name: raw.htmlName, num, idx: i });
|
|
775
|
+
} else {
|
|
776
|
+
let matchIdx = -1;
|
|
777
|
+
for (let j = stack.length - 1; j >= 0; j--) {
|
|
778
|
+
if (stack[j].name === raw.htmlName) {
|
|
779
|
+
matchIdx = j;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (matchIdx >= 0) {
|
|
784
|
+
const openEntry = stack[matchIdx];
|
|
785
|
+
stack.splice(matchIdx, 1);
|
|
786
|
+
const openRaw = matches[openEntry.idx];
|
|
787
|
+
result.push({
|
|
788
|
+
start: openRaw.start,
|
|
789
|
+
end: openRaw.end,
|
|
790
|
+
tagName: openRaw.htmlName,
|
|
791
|
+
tagNumber: openEntry.num,
|
|
792
|
+
isClosing: false,
|
|
793
|
+
isSelfClosing: false,
|
|
794
|
+
originalText: openRaw.text,
|
|
795
|
+
displayText: `<${openEntry.num}>`
|
|
796
|
+
});
|
|
797
|
+
result.push({
|
|
798
|
+
start: raw.start,
|
|
799
|
+
end: raw.end,
|
|
800
|
+
tagName: raw.htmlName,
|
|
801
|
+
tagNumber: openEntry.num,
|
|
802
|
+
isClosing: true,
|
|
803
|
+
isSelfClosing: false,
|
|
804
|
+
originalText: raw.text,
|
|
805
|
+
displayText: `</${openEntry.num}>`
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
result.sort((a, b) => a.start - b.start);
|
|
811
|
+
return result;
|
|
812
|
+
}
|
|
813
|
+
function computeHighlightSegments(text, rules) {
|
|
814
|
+
const rawRanges = [];
|
|
815
|
+
for (const rule of rules) {
|
|
816
|
+
if (rule.type === "spellcheck") {
|
|
817
|
+
for (const v of rule.validations) {
|
|
818
|
+
if (v.start < 0 || v.start >= v.end || !v.content) continue;
|
|
819
|
+
let matchStart = -1;
|
|
820
|
+
let matchEnd = -1;
|
|
821
|
+
if (v.end <= text.length && text.slice(v.start, v.end) === v.content) {
|
|
822
|
+
matchStart = v.start;
|
|
823
|
+
matchEnd = v.end;
|
|
824
|
+
} else {
|
|
825
|
+
const searchRadius = Math.max(64, v.content.length * 4);
|
|
826
|
+
const searchFrom = Math.max(0, v.start - searchRadius);
|
|
827
|
+
const searchTo = Math.min(text.length, v.end + searchRadius);
|
|
828
|
+
const regionLower = text.slice(searchFrom, searchTo).toLowerCase();
|
|
829
|
+
const contentLower = v.content.toLowerCase();
|
|
830
|
+
const idx = regionLower.indexOf(contentLower);
|
|
831
|
+
if (idx !== -1) {
|
|
832
|
+
matchStart = searchFrom + idx;
|
|
833
|
+
matchEnd = matchStart + v.content.length;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (matchStart >= 0) {
|
|
837
|
+
rawRanges.push({
|
|
838
|
+
start: matchStart,
|
|
839
|
+
end: matchEnd,
|
|
840
|
+
annotation: {
|
|
841
|
+
type: "spellcheck",
|
|
842
|
+
id: `sc-${matchStart}-${matchEnd}`,
|
|
843
|
+
data: v
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
} else if (rule.type === "glossary") {
|
|
849
|
+
const { label, entries } = rule;
|
|
850
|
+
for (const entry of entries) {
|
|
851
|
+
if (!entry.term && !entry.pattern) continue;
|
|
852
|
+
if (entry.pattern) {
|
|
853
|
+
let re;
|
|
854
|
+
try {
|
|
855
|
+
re = new RegExp(entry.pattern, "g");
|
|
856
|
+
} catch {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
let m;
|
|
860
|
+
while ((m = re.exec(text)) !== null) {
|
|
861
|
+
rawRanges.push({
|
|
862
|
+
start: m.index,
|
|
863
|
+
end: m.index + m[0].length,
|
|
864
|
+
annotation: {
|
|
865
|
+
type: "glossary",
|
|
866
|
+
id: `gl-${label}-${m.index}-${m.index + m[0].length}`,
|
|
867
|
+
data: {
|
|
868
|
+
label,
|
|
869
|
+
term: entry.term || entry.pattern,
|
|
870
|
+
description: entry.description
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
let idx = 0;
|
|
878
|
+
while ((idx = text.indexOf(entry.term, idx)) !== -1) {
|
|
879
|
+
rawRanges.push({
|
|
880
|
+
start: idx,
|
|
881
|
+
end: idx + entry.term.length,
|
|
882
|
+
annotation: {
|
|
883
|
+
type: "glossary",
|
|
884
|
+
id: `gl-${label}-${idx}-${idx + entry.term.length}`,
|
|
885
|
+
data: {
|
|
886
|
+
label,
|
|
887
|
+
term: entry.term,
|
|
888
|
+
description: entry.description
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
idx += entry.term.length;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} else if (rule.type === "tag") {
|
|
897
|
+
const pairs = rule.pattern ? detectCustomTags(text, rule.pattern) : detectAndPairTags(text, rule.detectInner ?? true);
|
|
898
|
+
for (const p of pairs) {
|
|
899
|
+
const isHtml = !rule.pattern || p.tagName !== p.originalText;
|
|
900
|
+
rawRanges.push({
|
|
901
|
+
start: p.start,
|
|
902
|
+
end: p.end,
|
|
903
|
+
annotation: {
|
|
904
|
+
type: "tag",
|
|
905
|
+
id: `tag-${p.start}-${p.end}`,
|
|
906
|
+
data: {
|
|
907
|
+
tagNumber: p.tagNumber,
|
|
908
|
+
tagName: p.tagName,
|
|
909
|
+
isClosing: p.isClosing,
|
|
910
|
+
isSelfClosing: p.isSelfClosing,
|
|
911
|
+
originalText: p.originalText,
|
|
912
|
+
displayText: p.displayText,
|
|
913
|
+
isHtml
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
} else if (rule.type === "quote") {
|
|
919
|
+
const quoteMap = detectQuotes(text, rule.detectOptions);
|
|
920
|
+
const seen = /* @__PURE__ */ new Set();
|
|
921
|
+
for (const [, qr] of quoteMap) {
|
|
922
|
+
if (seen.has(qr.start)) continue;
|
|
923
|
+
seen.add(qr.start);
|
|
924
|
+
const mapping = qr.quoteType === "single" ? rule.singleQuote : rule.doubleQuote;
|
|
925
|
+
const originalChar = qr.quoteType === "single" ? "'" : '"';
|
|
926
|
+
rawRanges.push({
|
|
927
|
+
start: qr.start,
|
|
928
|
+
end: qr.start + 1,
|
|
929
|
+
annotation: {
|
|
930
|
+
type: "quote",
|
|
931
|
+
id: `q-${qr.quoteType}-open-${qr.start}`,
|
|
932
|
+
data: {
|
|
933
|
+
quoteType: qr.quoteType,
|
|
934
|
+
position: "opening",
|
|
935
|
+
originalChar,
|
|
936
|
+
replacementChar: mapping.opening
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
if (qr.closed && qr.end !== null) {
|
|
941
|
+
rawRanges.push({
|
|
942
|
+
start: qr.end,
|
|
943
|
+
end: qr.end + 1,
|
|
944
|
+
annotation: {
|
|
945
|
+
type: "quote",
|
|
946
|
+
id: `q-${qr.quoteType}-close-${qr.end}`,
|
|
947
|
+
data: {
|
|
948
|
+
quoteType: qr.quoteType,
|
|
949
|
+
position: "closing",
|
|
950
|
+
originalChar,
|
|
951
|
+
replacementChar: mapping.closing
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
} else if (rule.type === "special-char") {
|
|
958
|
+
const allEntries = [...rule.entries];
|
|
959
|
+
for (const entry of allEntries) {
|
|
960
|
+
const flags = entry.pattern.flags.includes("g") ? entry.pattern.flags : entry.pattern.flags + "g";
|
|
961
|
+
const re = new RegExp(entry.pattern.source, flags);
|
|
962
|
+
let m;
|
|
963
|
+
while ((m = re.exec(text)) !== null) {
|
|
964
|
+
const matchStr = m[0];
|
|
965
|
+
const cp = matchStr.split("").map(
|
|
966
|
+
(c) => "U+" + (c.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")
|
|
967
|
+
).join(" ");
|
|
968
|
+
rawRanges.push({
|
|
969
|
+
start: m.index,
|
|
970
|
+
end: m.index + matchStr.length,
|
|
971
|
+
annotation: {
|
|
972
|
+
type: "special-char",
|
|
973
|
+
id: `sp-${m.index}-${m.index + matchStr.length}`,
|
|
974
|
+
data: { name: entry.name, char: matchStr, codePoint: cp }
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} else if (rule.type === "link") {
|
|
980
|
+
const defaultPattern = String.raw`https?:\/\/[^\s<>"']+|www\.[^\s<>"']+`;
|
|
981
|
+
const patternSource = rule.pattern ?? defaultPattern;
|
|
982
|
+
let re;
|
|
983
|
+
try {
|
|
984
|
+
re = new RegExp(patternSource, "gi");
|
|
985
|
+
} catch {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
let m;
|
|
989
|
+
while ((m = re.exec(text)) !== null) {
|
|
990
|
+
if (m[0].length === 0) {
|
|
991
|
+
re.lastIndex++;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
let matched = m[0];
|
|
995
|
+
const trailingPunct = /[.,;:!?)}\]]+$/;
|
|
996
|
+
const trailingMatch = trailingPunct.exec(matched);
|
|
997
|
+
if (trailingMatch) {
|
|
998
|
+
matched = matched.slice(0, -trailingMatch[0].length);
|
|
999
|
+
}
|
|
1000
|
+
const end = m.index + matched.length;
|
|
1001
|
+
const url = matched.startsWith("www.") ? "https://" + matched : matched;
|
|
1002
|
+
rawRanges.push({
|
|
1003
|
+
start: m.index,
|
|
1004
|
+
end,
|
|
1005
|
+
annotation: {
|
|
1006
|
+
type: "link",
|
|
1007
|
+
id: `link-${m.index}-${end}`,
|
|
1008
|
+
data: {
|
|
1009
|
+
url,
|
|
1010
|
+
displayText: matched
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (rawRanges.length === 0) return [];
|
|
1018
|
+
const tagRules = rules.filter((r) => r.type === "tag");
|
|
1019
|
+
const tagsCollapsed = tagRules.some((r) => r.collapsed);
|
|
1020
|
+
const tagRanges = rawRanges.filter((r) => r.annotation.type === "tag");
|
|
1021
|
+
const quoteDetectInTags = rules.filter((r) => r.type === "quote").some((r) => r.detectInTags);
|
|
1022
|
+
let filteredRanges = rawRanges;
|
|
1023
|
+
if (tagRanges.length > 0) {
|
|
1024
|
+
filteredRanges = rawRanges.filter((r) => {
|
|
1025
|
+
if (r.annotation.type === "tag") return true;
|
|
1026
|
+
if (r.annotation.type === "quote" && !quoteDetectInTags) {
|
|
1027
|
+
return !tagRanges.some((t) => r.start >= t.start && r.end <= t.end);
|
|
1028
|
+
}
|
|
1029
|
+
if (tagsCollapsed) {
|
|
1030
|
+
return !tagRanges.some((t) => r.start < t.end && r.end > t.start);
|
|
1031
|
+
}
|
|
1032
|
+
return true;
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const points = /* @__PURE__ */ new Set();
|
|
1036
|
+
for (const r of filteredRanges) {
|
|
1037
|
+
points.add(r.start);
|
|
1038
|
+
points.add(r.end);
|
|
1039
|
+
}
|
|
1040
|
+
const sortedPoints = [...points].sort((a, b) => a - b);
|
|
1041
|
+
const segments = [];
|
|
1042
|
+
for (let i = 0; i < sortedPoints.length - 1; i++) {
|
|
1043
|
+
const segStart = sortedPoints[i];
|
|
1044
|
+
const segEnd = sortedPoints[i + 1];
|
|
1045
|
+
const annotations = filteredRanges.filter((r) => r.start <= segStart && r.end >= segEnd).map((r) => r.annotation);
|
|
1046
|
+
if (annotations.length > 0) {
|
|
1047
|
+
segments.push({ start: segStart, end: segEnd, annotations });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return segments;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/layout/cat-editor/selection-helpers.ts
|
|
1054
|
+
import { $getRoot } from "lexical";
|
|
1055
|
+
function $isCEFalseToken(node) {
|
|
1056
|
+
const types = node.__highlightTypes.split(",");
|
|
1057
|
+
if (types.includes("tag-collapsed")) return true;
|
|
1058
|
+
if (types.includes("quote") && node.__displayText) return true;
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
function $pointToGlobalOffset(nodeKey, offset) {
|
|
1062
|
+
const root = $getRoot();
|
|
1063
|
+
const paragraphs = root.getChildren();
|
|
1064
|
+
let global = 0;
|
|
1065
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
1066
|
+
if (pi > 0) global += 1;
|
|
1067
|
+
const p = paragraphs[pi];
|
|
1068
|
+
if (p.getKey() === nodeKey) {
|
|
1069
|
+
if ("getChildren" in p) {
|
|
1070
|
+
const children = p.getChildren();
|
|
1071
|
+
let childChars = 0;
|
|
1072
|
+
for (let ci = 0; ci < Math.min(offset, children.length); ci++) {
|
|
1073
|
+
const child = children[ci];
|
|
1074
|
+
if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
|
|
1075
|
+
continue;
|
|
1076
|
+
childChars += child.getTextContent().length;
|
|
1077
|
+
}
|
|
1078
|
+
return global + childChars;
|
|
1079
|
+
}
|
|
1080
|
+
return global;
|
|
1081
|
+
}
|
|
1082
|
+
if (!("getChildren" in p)) continue;
|
|
1083
|
+
for (const child of p.getChildren()) {
|
|
1084
|
+
const isNlMarker = $isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX);
|
|
1085
|
+
if (child.getKey() === nodeKey) {
|
|
1086
|
+
if (isNlMarker) return global;
|
|
1087
|
+
if ($isHighlightNode(child) && child.getMode() === "token") {
|
|
1088
|
+
return global + (offset > 0 ? child.getTextContent().length : 0);
|
|
1089
|
+
}
|
|
1090
|
+
if ($isMentionNode(child)) {
|
|
1091
|
+
return global + (offset > 0 ? child.getTextContent().length : 0);
|
|
1092
|
+
}
|
|
1093
|
+
return global + offset;
|
|
1094
|
+
}
|
|
1095
|
+
if (isNlMarker) continue;
|
|
1096
|
+
global += child.getTextContent().length;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return global;
|
|
1100
|
+
}
|
|
1101
|
+
function $globalOffsetToPoint(target) {
|
|
1102
|
+
const root = $getRoot();
|
|
1103
|
+
const paragraphs = root.getChildren();
|
|
1104
|
+
let remaining = target;
|
|
1105
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
1106
|
+
if (pi > 0) {
|
|
1107
|
+
if (remaining <= 0) {
|
|
1108
|
+
const p2 = paragraphs[pi];
|
|
1109
|
+
if ("getChildren" in p2) {
|
|
1110
|
+
for (const child of p2.getChildren()) {
|
|
1111
|
+
if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
|
|
1112
|
+
continue;
|
|
1113
|
+
return { key: child.getKey(), offset: 0, type: "text" };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return { key: paragraphs[pi].getKey(), offset: 0, type: "element" };
|
|
1117
|
+
}
|
|
1118
|
+
remaining -= 1;
|
|
1119
|
+
}
|
|
1120
|
+
const p = paragraphs[pi];
|
|
1121
|
+
if (!("getChildren" in p)) continue;
|
|
1122
|
+
const allChildren = p.getChildren();
|
|
1123
|
+
for (let ci = 0; ci < allChildren.length; ci++) {
|
|
1124
|
+
const child = allChildren[ci];
|
|
1125
|
+
if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
|
|
1126
|
+
continue;
|
|
1127
|
+
const len = child.getTextContent().length;
|
|
1128
|
+
if ($isHighlightNode(child) && $isCEFalseToken(child)) {
|
|
1129
|
+
if (remaining <= 0) {
|
|
1130
|
+
return { key: p.getKey(), offset: ci, type: "element" };
|
|
1131
|
+
}
|
|
1132
|
+
remaining -= len;
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if ($isMentionNode(child)) {
|
|
1136
|
+
if (remaining <= 0) {
|
|
1137
|
+
return { key: p.getKey(), offset: ci, type: "element" };
|
|
1138
|
+
}
|
|
1139
|
+
remaining -= len;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if ($isHighlightNode(child) && child.getMode() === "token") {
|
|
1143
|
+
if (remaining > len) {
|
|
1144
|
+
remaining -= len;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
key: child.getKey(),
|
|
1149
|
+
offset: remaining <= 0 ? 0 : remaining >= len ? len : remaining <= len / 2 ? 0 : len,
|
|
1150
|
+
type: "text"
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
if (remaining <= len) {
|
|
1154
|
+
return {
|
|
1155
|
+
key: child.getKey(),
|
|
1156
|
+
offset: Math.max(0, remaining),
|
|
1157
|
+
type: "text"
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
remaining -= len;
|
|
1161
|
+
}
|
|
1162
|
+
if (remaining <= 0) {
|
|
1163
|
+
let afterIdx = allChildren.length;
|
|
1164
|
+
for (let ci = allChildren.length - 1; ci >= 0; ci--) {
|
|
1165
|
+
const c = allChildren[ci];
|
|
1166
|
+
if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
|
|
1167
|
+
afterIdx = ci;
|
|
1168
|
+
} else {
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return { key: p.getKey(), offset: afterIdx, type: "element" };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
for (let pi = paragraphs.length - 1; pi >= 0; pi--) {
|
|
1176
|
+
const p = paragraphs[pi];
|
|
1177
|
+
if ("getChildren" in p) {
|
|
1178
|
+
const allChildren = p.getChildren();
|
|
1179
|
+
for (let ci = allChildren.length - 1; ci >= 0; ci--) {
|
|
1180
|
+
const child = allChildren[ci];
|
|
1181
|
+
if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
|
|
1182
|
+
continue;
|
|
1183
|
+
if ($isHighlightNode(child) && $isCEFalseToken(child)) continue;
|
|
1184
|
+
if ($isMentionNode(child)) continue;
|
|
1185
|
+
return {
|
|
1186
|
+
key: child.getKey(),
|
|
1187
|
+
offset: child.getTextContent().length,
|
|
1188
|
+
type: "text"
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
let afterIdx = allChildren.length;
|
|
1192
|
+
for (let ci = allChildren.length - 1; ci >= 0; ci--) {
|
|
1193
|
+
const c = allChildren[ci];
|
|
1194
|
+
if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
|
|
1195
|
+
afterIdx = ci;
|
|
1196
|
+
} else {
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return { key: p.getKey(), offset: afterIdx, type: "element" };
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/layout/cat-editor/plugins.tsx
|
|
1207
|
+
function HighlightsPlugin({
|
|
1208
|
+
rules,
|
|
1209
|
+
annotationMapRef,
|
|
1210
|
+
codepointDisplayMap
|
|
1211
|
+
}) {
|
|
1212
|
+
const [editor] = useLexicalComposerContext2();
|
|
1213
|
+
const rafRef = useRef2(null);
|
|
1214
|
+
const applyHighlights = useCallback2(() => {
|
|
1215
|
+
setCodepointOverrides(codepointDisplayMap);
|
|
1216
|
+
const editorElement = editor.getRootElement();
|
|
1217
|
+
const editorHasFocus = editorElement != null && editorElement.contains(editorElement.ownerDocument.activeElement);
|
|
1218
|
+
editor.update(
|
|
1219
|
+
() => {
|
|
1220
|
+
$addUpdateTag("cat-highlights");
|
|
1221
|
+
const root = $getRoot2();
|
|
1222
|
+
const paragraphs = root.getChildren();
|
|
1223
|
+
const lines = [];
|
|
1224
|
+
const savedMentions = [];
|
|
1225
|
+
let collectOffset = 0;
|
|
1226
|
+
for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) {
|
|
1227
|
+
const p = paragraphs[pIdx];
|
|
1228
|
+
let lineText = "";
|
|
1229
|
+
if ("getChildren" in p) {
|
|
1230
|
+
for (const child of p.getChildren()) {
|
|
1231
|
+
if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if ($isMentionNode(child)) {
|
|
1235
|
+
const text = child.getTextContent();
|
|
1236
|
+
savedMentions.push({
|
|
1237
|
+
start: collectOffset + lineText.length,
|
|
1238
|
+
end: collectOffset + lineText.length + text.length,
|
|
1239
|
+
mentionId: child.__mentionId,
|
|
1240
|
+
mentionName: child.__mentionName,
|
|
1241
|
+
text
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
lineText += child.getTextContent();
|
|
1245
|
+
}
|
|
1246
|
+
} else {
|
|
1247
|
+
lineText = p.getTextContent();
|
|
1248
|
+
}
|
|
1249
|
+
lines.push(lineText);
|
|
1250
|
+
collectOffset += lineText.length + 1;
|
|
1251
|
+
}
|
|
1252
|
+
const fullText = lines.join("\n");
|
|
1253
|
+
const mentionRule = rules.find(
|
|
1254
|
+
(r) => r.type === "mention"
|
|
1255
|
+
);
|
|
1256
|
+
if (mentionRule) {
|
|
1257
|
+
const pattern = getMentionPattern();
|
|
1258
|
+
let match;
|
|
1259
|
+
while ((match = pattern.exec(fullText)) !== null) {
|
|
1260
|
+
const matchStart = match.index;
|
|
1261
|
+
const matchEnd = matchStart + match[0].length;
|
|
1262
|
+
const matchId = match[1];
|
|
1263
|
+
const alreadySaved = savedMentions.some(
|
|
1264
|
+
(m) => m.start === matchStart && m.end === matchEnd
|
|
1265
|
+
);
|
|
1266
|
+
if (alreadySaved) continue;
|
|
1267
|
+
const user = mentionRule.users.find((u) => u.id === matchId);
|
|
1268
|
+
if (user) {
|
|
1269
|
+
savedMentions.push({
|
|
1270
|
+
start: matchStart,
|
|
1271
|
+
end: matchEnd,
|
|
1272
|
+
mentionId: user.id,
|
|
1273
|
+
mentionName: user.name,
|
|
1274
|
+
text: match[0]
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
let segmentText = fullText;
|
|
1280
|
+
for (let mi = savedMentions.length - 1; mi >= 0; mi--) {
|
|
1281
|
+
const m = savedMentions[mi];
|
|
1282
|
+
segmentText = segmentText.slice(0, m.start) + "".repeat(m.end - m.start) + segmentText.slice(m.end);
|
|
1283
|
+
}
|
|
1284
|
+
const segments = computeHighlightSegments(segmentText, rules);
|
|
1285
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1286
|
+
for (const seg of segments) {
|
|
1287
|
+
for (const ann of seg.annotations) {
|
|
1288
|
+
newMap.set(ann.id, ann);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
annotationMapRef.current = newMap;
|
|
1292
|
+
const prevSelection = $getSelection();
|
|
1293
|
+
let savedAnchor = null;
|
|
1294
|
+
let savedFocus = null;
|
|
1295
|
+
if ($isRangeSelection(prevSelection)) {
|
|
1296
|
+
savedAnchor = $pointToGlobalOffset(
|
|
1297
|
+
prevSelection.anchor.key,
|
|
1298
|
+
prevSelection.anchor.offset
|
|
1299
|
+
);
|
|
1300
|
+
savedFocus = $pointToGlobalOffset(
|
|
1301
|
+
prevSelection.focus.key,
|
|
1302
|
+
prevSelection.focus.offset
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
root.clear();
|
|
1306
|
+
if (fullText.length === 0) {
|
|
1307
|
+
const p = $createParagraphNode();
|
|
1308
|
+
p.append($createTextNode2(""));
|
|
1309
|
+
root.append(p);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const emittedMentionStarts = /* @__PURE__ */ new Set();
|
|
1313
|
+
const appendWithMentions = (paragraph, rangeStart, rangeEnd, makeNode) => {
|
|
1314
|
+
const overlapping = savedMentions.filter(
|
|
1315
|
+
(m) => m.start < rangeEnd && m.end > rangeStart && !emittedMentionStarts.has(m.start)
|
|
1316
|
+
);
|
|
1317
|
+
if (overlapping.length === 0) {
|
|
1318
|
+
const text = fullText.slice(rangeStart, rangeEnd);
|
|
1319
|
+
if (text.length > 0) {
|
|
1320
|
+
paragraph.append(makeNode(text));
|
|
1321
|
+
}
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
overlapping.sort((a, b) => a.start - b.start);
|
|
1325
|
+
let cursor = rangeStart;
|
|
1326
|
+
for (const m of overlapping) {
|
|
1327
|
+
const mStart = Math.max(m.start, rangeStart);
|
|
1328
|
+
const mEnd = Math.min(m.end, rangeEnd);
|
|
1329
|
+
if (mStart > cursor) {
|
|
1330
|
+
const beforeText = fullText.slice(cursor, mStart);
|
|
1331
|
+
if (beforeText.length > 0) {
|
|
1332
|
+
paragraph.append(makeNode(beforeText));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
paragraph.append(
|
|
1336
|
+
$createMentionNode(m.mentionId, m.mentionName, m.text)
|
|
1337
|
+
);
|
|
1338
|
+
emittedMentionStarts.add(m.start);
|
|
1339
|
+
cursor = mEnd;
|
|
1340
|
+
}
|
|
1341
|
+
if (cursor < rangeEnd) {
|
|
1342
|
+
const afterText = fullText.slice(cursor, rangeEnd);
|
|
1343
|
+
if (afterText.length > 0) {
|
|
1344
|
+
paragraph.append(makeNode(afterText));
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const textLines = fullText.split("\n");
|
|
1349
|
+
let globalOffset = 0;
|
|
1350
|
+
for (const line of textLines) {
|
|
1351
|
+
const paragraph = $createParagraphNode();
|
|
1352
|
+
const lineStart = globalOffset;
|
|
1353
|
+
const lineEnd = globalOffset + line.length;
|
|
1354
|
+
const lineSegments = segments.filter(
|
|
1355
|
+
(s) => s.start < lineEnd && s.end > lineStart
|
|
1356
|
+
);
|
|
1357
|
+
let pos = lineStart;
|
|
1358
|
+
for (const seg of lineSegments) {
|
|
1359
|
+
const sStart = Math.max(seg.start, lineStart);
|
|
1360
|
+
const sEnd = Math.min(seg.end, lineEnd);
|
|
1361
|
+
if (sStart > pos) {
|
|
1362
|
+
appendWithMentions(
|
|
1363
|
+
paragraph,
|
|
1364
|
+
pos,
|
|
1365
|
+
sStart,
|
|
1366
|
+
(text) => $createTextNode2(text)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const tagAnn = seg.annotations.find((a) => a.type === "tag");
|
|
1370
|
+
const tagRule = rules.find((r) => r.type === "tag");
|
|
1371
|
+
const tagsCollapsed = !!tagRule?.collapsed;
|
|
1372
|
+
const collapseScope = tagRule?.collapseScope ?? "all";
|
|
1373
|
+
const thisTagCollapsed = tagsCollapsed && !!tagAnn && (collapseScope === "all" || tagAnn.data.isHtml);
|
|
1374
|
+
const typesArr = [
|
|
1375
|
+
...new Set(
|
|
1376
|
+
seg.annotations.map((a) => {
|
|
1377
|
+
if (a.type === "glossary") return `glossary-${a.data.label}`;
|
|
1378
|
+
if (a.type === "spellcheck")
|
|
1379
|
+
return `spellcheck-${a.data.categoryId}`;
|
|
1380
|
+
return a.type;
|
|
1381
|
+
})
|
|
1382
|
+
)
|
|
1383
|
+
];
|
|
1384
|
+
if (thisTagCollapsed) {
|
|
1385
|
+
typesArr.push("tag-collapsed");
|
|
1386
|
+
}
|
|
1387
|
+
const types = typesArr.join(",");
|
|
1388
|
+
const ids = seg.annotations.map((a) => a.id).join(",");
|
|
1389
|
+
const tagDisplayText = tagAnn?.type === "tag" && thisTagCollapsed ? tagAnn.data.displayText : void 0;
|
|
1390
|
+
const isTagToken = thisTagCollapsed;
|
|
1391
|
+
const quoteAnn = seg.annotations.find((a) => a.type === "quote");
|
|
1392
|
+
const quoteDisplayText = quoteAnn?.type === "quote" ? quoteAnn.data.replacementChar : void 0;
|
|
1393
|
+
const isQuoteToken = !!quoteAnn;
|
|
1394
|
+
const containingMention = savedMentions.find(
|
|
1395
|
+
(m) => m.start <= sStart && m.end >= sEnd
|
|
1396
|
+
);
|
|
1397
|
+
if (containingMention) {
|
|
1398
|
+
if (!emittedMentionStarts.has(containingMention.start)) {
|
|
1399
|
+
paragraph.append(
|
|
1400
|
+
$createMentionNode(
|
|
1401
|
+
containingMention.mentionId,
|
|
1402
|
+
containingMention.mentionName,
|
|
1403
|
+
containingMention.text
|
|
1404
|
+
)
|
|
1405
|
+
);
|
|
1406
|
+
emittedMentionStarts.add(containingMention.start);
|
|
1407
|
+
}
|
|
1408
|
+
} else {
|
|
1409
|
+
appendWithMentions(
|
|
1410
|
+
paragraph,
|
|
1411
|
+
sStart,
|
|
1412
|
+
sEnd,
|
|
1413
|
+
(text) => $createHighlightNode(
|
|
1414
|
+
text,
|
|
1415
|
+
types,
|
|
1416
|
+
ids,
|
|
1417
|
+
tagDisplayText ?? quoteDisplayText,
|
|
1418
|
+
isTagToken || isQuoteToken
|
|
1419
|
+
)
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
pos = sEnd;
|
|
1423
|
+
}
|
|
1424
|
+
if (pos < lineEnd) {
|
|
1425
|
+
appendWithMentions(
|
|
1426
|
+
paragraph,
|
|
1427
|
+
pos,
|
|
1428
|
+
lineEnd,
|
|
1429
|
+
(text) => $createTextNode2(text)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
const nlPos = lineEnd;
|
|
1433
|
+
if (nlPos < fullText.length && fullText[nlPos] === "\n") {
|
|
1434
|
+
const nlSegments = segments.filter(
|
|
1435
|
+
(s) => s.start <= nlPos && s.end > nlPos
|
|
1436
|
+
);
|
|
1437
|
+
if (nlSegments.length > 0) {
|
|
1438
|
+
const nlAnns = nlSegments.flatMap((s) => s.annotations);
|
|
1439
|
+
const types = [
|
|
1440
|
+
...new Set(
|
|
1441
|
+
nlAnns.map((a) => {
|
|
1442
|
+
if (a.type === "glossary") return `glossary-${a.data.label}`;
|
|
1443
|
+
if (a.type === "spellcheck")
|
|
1444
|
+
return `spellcheck-${a.data.categoryId}`;
|
|
1445
|
+
return a.type;
|
|
1446
|
+
})
|
|
1447
|
+
)
|
|
1448
|
+
].join(",");
|
|
1449
|
+
const ids = nlAnns.map((a) => a.id).join(",");
|
|
1450
|
+
const symbol = CODEPOINT_DISPLAY_MAP[10];
|
|
1451
|
+
if (paragraph.getChildrenSize() === 0) {
|
|
1452
|
+
paragraph.append($createTextNode2(""));
|
|
1453
|
+
}
|
|
1454
|
+
paragraph.append(
|
|
1455
|
+
$createHighlightNode(symbol, types, NL_MARKER_PREFIX + ids)
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (paragraph.getChildrenSize() === 0) {
|
|
1460
|
+
paragraph.append($createTextNode2(""));
|
|
1461
|
+
}
|
|
1462
|
+
root.append(paragraph);
|
|
1463
|
+
globalOffset = lineEnd + 1;
|
|
1464
|
+
}
|
|
1465
|
+
if (editorHasFocus && savedAnchor !== null && savedFocus !== null) {
|
|
1466
|
+
const anchorPt = $globalOffsetToPoint(savedAnchor);
|
|
1467
|
+
const focusPt = $globalOffsetToPoint(savedFocus);
|
|
1468
|
+
if (anchorPt && focusPt) {
|
|
1469
|
+
const sel = $createRangeSelection();
|
|
1470
|
+
sel.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
|
|
1471
|
+
sel.focus.set(focusPt.key, focusPt.offset, focusPt.type);
|
|
1472
|
+
$setSelection(sel);
|
|
1473
|
+
}
|
|
1474
|
+
} else {
|
|
1475
|
+
$setSelection(null);
|
|
1476
|
+
}
|
|
1477
|
+
},
|
|
1478
|
+
{ tag: "historic" }
|
|
1479
|
+
);
|
|
1480
|
+
}, [editor, rules, annotationMapRef]);
|
|
1481
|
+
useEffect2(() => {
|
|
1482
|
+
applyHighlights();
|
|
1483
|
+
}, [applyHighlights]);
|
|
1484
|
+
useEffect2(() => {
|
|
1485
|
+
const unregister = editor.registerUpdateListener(
|
|
1486
|
+
({ tags, dirtyElements, dirtyLeaves }) => {
|
|
1487
|
+
if (tags.has("cat-highlights")) return;
|
|
1488
|
+
if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
|
|
1489
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
1490
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
1491
|
+
applyHighlights();
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
return () => {
|
|
1496
|
+
unregister();
|
|
1497
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
1498
|
+
};
|
|
1499
|
+
}, [editor, applyHighlights]);
|
|
1500
|
+
useEffect2(() => {
|
|
1501
|
+
return editor.registerCommand(
|
|
1502
|
+
PASTE_COMMAND,
|
|
1503
|
+
(event) => {
|
|
1504
|
+
const clipboardData = event.clipboardData;
|
|
1505
|
+
if (!clipboardData) return false;
|
|
1506
|
+
const text = clipboardData.getData("text/plain");
|
|
1507
|
+
if (text && text !== text.replace(/\n+$/, "")) {
|
|
1508
|
+
event.preventDefault();
|
|
1509
|
+
const trimmed = text.replace(/\n+$/, "");
|
|
1510
|
+
editor.update(() => {
|
|
1511
|
+
const selection = $getSelection();
|
|
1512
|
+
if ($isRangeSelection(selection)) {
|
|
1513
|
+
selection.insertRawText(trimmed);
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
return false;
|
|
1519
|
+
},
|
|
1520
|
+
COMMAND_PRIORITY_HIGH
|
|
1521
|
+
);
|
|
1522
|
+
}, [editor]);
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
function EditorRefPlugin({
|
|
1526
|
+
editorRef,
|
|
1527
|
+
savedSelectionRef
|
|
1528
|
+
}) {
|
|
1529
|
+
const [editor] = useLexicalComposerContext2();
|
|
1530
|
+
useEffect2(() => {
|
|
1531
|
+
editorRef.current = editor;
|
|
1532
|
+
}, [editor, editorRef]);
|
|
1533
|
+
useEffect2(() => {
|
|
1534
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
1535
|
+
editorState.read(() => {
|
|
1536
|
+
const sel = $getSelection();
|
|
1537
|
+
if ($isRangeSelection(sel)) {
|
|
1538
|
+
savedSelectionRef.current = {
|
|
1539
|
+
anchor: $pointToGlobalOffset(sel.anchor.key, sel.anchor.offset),
|
|
1540
|
+
focus: $pointToGlobalOffset(sel.focus.key, sel.focus.offset)
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
}, [editor, savedSelectionRef]);
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
function $isNonEditableNode(node) {
|
|
1549
|
+
if (!$isHighlightNode(node)) return false;
|
|
1550
|
+
if (node.__ruleIds.startsWith(NL_MARKER_PREFIX)) return true;
|
|
1551
|
+
const types = node.__highlightTypes.split(",");
|
|
1552
|
+
if (types.includes("tag-collapsed")) return true;
|
|
1553
|
+
if (types.includes("quote") && node.__displayText) return true;
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
function $clampPointAwayFromNonEditable(point) {
|
|
1557
|
+
if (point.type === "element") return null;
|
|
1558
|
+
const node = point.getNode();
|
|
1559
|
+
if (!$isNonEditableNode(node)) return null;
|
|
1560
|
+
const parent = node.getParent();
|
|
1561
|
+
if (parent && "getChildren" in parent) {
|
|
1562
|
+
const siblings = parent.getChildren();
|
|
1563
|
+
const idx = siblings.findIndex((s) => s.getKey() === node.getKey());
|
|
1564
|
+
if (idx >= 0) {
|
|
1565
|
+
const elemOffset = point.offset > 0 ? idx + 1 : idx;
|
|
1566
|
+
return {
|
|
1567
|
+
key: parent.getKey(),
|
|
1568
|
+
offset: elemOffset,
|
|
1569
|
+
type: "element"
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
function $findNextEditable(startNode) {
|
|
1576
|
+
if ($isHighlightNode(startNode) && startNode.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
|
|
1577
|
+
const paragraph2 = startNode.getParent();
|
|
1578
|
+
const nextParagraph = paragraph2?.getNextSibling();
|
|
1579
|
+
if (nextParagraph && "getChildren" in nextParagraph) {
|
|
1580
|
+
const first = nextParagraph.getFirstChild();
|
|
1581
|
+
if (first && !$isNonEditableNode(first)) {
|
|
1582
|
+
return { key: first.getKey(), offset: 0, type: "text" };
|
|
1583
|
+
}
|
|
1584
|
+
if (first) {
|
|
1585
|
+
return {
|
|
1586
|
+
key: nextParagraph.getKey(),
|
|
1587
|
+
offset: 0,
|
|
1588
|
+
type: "element"
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
const next = startNode.getNextSibling();
|
|
1595
|
+
if (next && !$isNonEditableNode(next)) {
|
|
1596
|
+
return { key: next.getKey(), offset: 0, type: "text" };
|
|
1597
|
+
}
|
|
1598
|
+
const paragraph = startNode.getParent();
|
|
1599
|
+
if (paragraph && "getChildren" in paragraph) {
|
|
1600
|
+
const children = paragraph.getChildren();
|
|
1601
|
+
const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
|
|
1602
|
+
if (idx >= 0) {
|
|
1603
|
+
return { key: paragraph.getKey(), offset: idx + 1, type: "element" };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
function $findPrevEditable(startNode) {
|
|
1609
|
+
const prev = startNode.getPreviousSibling();
|
|
1610
|
+
if (prev && !$isNonEditableNode(prev)) {
|
|
1611
|
+
return {
|
|
1612
|
+
key: prev.getKey(),
|
|
1613
|
+
offset: prev.getTextContent().length,
|
|
1614
|
+
type: "text"
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
const paragraph = startNode.getParent();
|
|
1618
|
+
if (paragraph && "getChildren" in paragraph) {
|
|
1619
|
+
const children = paragraph.getChildren();
|
|
1620
|
+
const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
|
|
1621
|
+
if (idx >= 0) {
|
|
1622
|
+
return { key: paragraph.getKey(), offset: idx, type: "element" };
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function NLMarkerNavigationPlugin() {
|
|
1628
|
+
const [editor] = useLexicalComposerContext2();
|
|
1629
|
+
useEffect2(() => {
|
|
1630
|
+
const unregRight = editor.registerCommand(
|
|
1631
|
+
KEY_ARROW_RIGHT_COMMAND,
|
|
1632
|
+
(event) => {
|
|
1633
|
+
const selection = $getSelection();
|
|
1634
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
1635
|
+
return false;
|
|
1636
|
+
const isRTL = $isParentElementRTL(selection);
|
|
1637
|
+
const { anchor } = selection;
|
|
1638
|
+
const node = anchor.getNode();
|
|
1639
|
+
let adjacentNode = null;
|
|
1640
|
+
if (isRTL) {
|
|
1641
|
+
if (anchor.type === "text") {
|
|
1642
|
+
if (anchor.offset > 0) return false;
|
|
1643
|
+
adjacentNode = node.getPreviousSibling();
|
|
1644
|
+
} else {
|
|
1645
|
+
const children = node.getChildren();
|
|
1646
|
+
adjacentNode = children[anchor.offset - 1] ?? null;
|
|
1647
|
+
}
|
|
1648
|
+
} else {
|
|
1649
|
+
if (anchor.type === "text") {
|
|
1650
|
+
if (anchor.offset < node.getTextContent().length) return false;
|
|
1651
|
+
adjacentNode = node.getNextSibling();
|
|
1652
|
+
} else {
|
|
1653
|
+
const children = node.getChildren();
|
|
1654
|
+
adjacentNode = children[anchor.offset] ?? null;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (adjacentNode && $isNonEditableNode(adjacentNode)) {
|
|
1658
|
+
const target = isRTL ? $findPrevEditable(adjacentNode) : $findNextEditable(adjacentNode);
|
|
1659
|
+
if (target) {
|
|
1660
|
+
selection.anchor.set(target.key, target.offset, target.type);
|
|
1661
|
+
selection.focus.set(target.key, target.offset, target.type);
|
|
1662
|
+
event.preventDefault();
|
|
1663
|
+
return true;
|
|
1664
|
+
}
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
if ($isNonEditableNode(node)) {
|
|
1668
|
+
const target = isRTL ? $findPrevEditable(node) : $findNextEditable(node);
|
|
1669
|
+
if (target) {
|
|
1670
|
+
selection.anchor.set(target.key, target.offset, target.type);
|
|
1671
|
+
selection.focus.set(target.key, target.offset, target.type);
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
return true;
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
return false;
|
|
1678
|
+
},
|
|
1679
|
+
COMMAND_PRIORITY_HIGH
|
|
1680
|
+
);
|
|
1681
|
+
const unregLeft = editor.registerCommand(
|
|
1682
|
+
KEY_ARROW_LEFT_COMMAND,
|
|
1683
|
+
(event) => {
|
|
1684
|
+
const selection = $getSelection();
|
|
1685
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
1686
|
+
return false;
|
|
1687
|
+
const isRTL = $isParentElementRTL(selection);
|
|
1688
|
+
const { anchor } = selection;
|
|
1689
|
+
const node = anchor.getNode();
|
|
1690
|
+
let adjacentNode = null;
|
|
1691
|
+
if (isRTL) {
|
|
1692
|
+
if (anchor.type === "text") {
|
|
1693
|
+
if (anchor.offset < node.getTextContent().length) return false;
|
|
1694
|
+
adjacentNode = node.getNextSibling();
|
|
1695
|
+
} else {
|
|
1696
|
+
const children = node.getChildren();
|
|
1697
|
+
adjacentNode = children[anchor.offset] ?? null;
|
|
1698
|
+
}
|
|
1699
|
+
} else {
|
|
1700
|
+
if (anchor.type === "text") {
|
|
1701
|
+
if (anchor.offset > 0) return false;
|
|
1702
|
+
adjacentNode = node.getPreviousSibling();
|
|
1703
|
+
} else {
|
|
1704
|
+
const children = node.getChildren();
|
|
1705
|
+
adjacentNode = children[anchor.offset - 1] ?? null;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (adjacentNode && $isNonEditableNode(adjacentNode)) {
|
|
1709
|
+
const target = isRTL ? $findNextEditable(adjacentNode) : $findPrevEditable(adjacentNode);
|
|
1710
|
+
if (target) {
|
|
1711
|
+
selection.anchor.set(target.key, target.offset, target.type);
|
|
1712
|
+
selection.focus.set(target.key, target.offset, target.type);
|
|
1713
|
+
event.preventDefault();
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
if ($isNonEditableNode(node)) {
|
|
1719
|
+
const target = isRTL ? $findNextEditable(node) : $findPrevEditable(node);
|
|
1720
|
+
if (target) {
|
|
1721
|
+
selection.anchor.set(target.key, target.offset, target.type);
|
|
1722
|
+
selection.focus.set(target.key, target.offset, target.type);
|
|
1723
|
+
event.preventDefault();
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
return false;
|
|
1729
|
+
},
|
|
1730
|
+
COMMAND_PRIORITY_HIGH
|
|
1731
|
+
);
|
|
1732
|
+
const unregSel = editor.registerCommand(
|
|
1733
|
+
SELECTION_CHANGE_COMMAND,
|
|
1734
|
+
() => {
|
|
1735
|
+
const selection = $getSelection();
|
|
1736
|
+
if (!$isRangeSelection(selection)) return false;
|
|
1737
|
+
const anchorFix = $clampPointAwayFromNonEditable(selection.anchor);
|
|
1738
|
+
const focusFix = $clampPointAwayFromNonEditable(selection.focus);
|
|
1739
|
+
if (anchorFix) {
|
|
1740
|
+
selection.anchor.set(anchorFix.key, anchorFix.offset, anchorFix.type);
|
|
1741
|
+
}
|
|
1742
|
+
if (focusFix) {
|
|
1743
|
+
selection.focus.set(focusFix.key, focusFix.offset, focusFix.type);
|
|
1744
|
+
}
|
|
1745
|
+
return false;
|
|
1746
|
+
},
|
|
1747
|
+
COMMAND_PRIORITY_HIGH
|
|
1748
|
+
);
|
|
1749
|
+
return () => {
|
|
1750
|
+
unregRight();
|
|
1751
|
+
unregLeft();
|
|
1752
|
+
unregSel();
|
|
1753
|
+
};
|
|
1754
|
+
}, [editor]);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/layout/cat-editor/popover.tsx
|
|
1759
|
+
import * as React from "react";
|
|
1760
|
+
import { useLayoutEffect, useRef as useRef3 } from "react";
|
|
1761
|
+
import { createPopper } from "@popperjs/core";
|
|
1762
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1763
|
+
function SpellCheckPopoverContent({
|
|
1764
|
+
data,
|
|
1765
|
+
onSuggestionClick
|
|
1766
|
+
}) {
|
|
1767
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-2.5 p-3 max-w-sm", children: [
|
|
1768
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
1769
|
+
/* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-spell", children: data.shortMessage || "Spelling" }),
|
|
1770
|
+
/* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: data.categoryId })
|
|
1771
|
+
] }),
|
|
1772
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground", children: data.message }),
|
|
1773
|
+
data.content && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
|
|
1774
|
+
"Found:",
|
|
1775
|
+
" ",
|
|
1776
|
+
/* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-destructive-foreground", children: data.content })
|
|
1777
|
+
] }),
|
|
1778
|
+
data.suggestions.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
|
|
1779
|
+
/* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground", children: "Suggestions:" }),
|
|
1780
|
+
/* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-1", children: data.suggestions.map((s, i) => /* @__PURE__ */ jsx2(
|
|
1781
|
+
"button",
|
|
1782
|
+
{
|
|
1783
|
+
type: "button",
|
|
1784
|
+
className: "cat-suggestion-btn",
|
|
1785
|
+
onClick: () => onSuggestionClick(s.value),
|
|
1786
|
+
children: s.value
|
|
1787
|
+
},
|
|
1788
|
+
i
|
|
1789
|
+
)) })
|
|
1790
|
+
] }),
|
|
1791
|
+
data.dictionaries && data.dictionaries.length > 0 && /* @__PURE__ */ jsxs2("p", { className: "text-[11px] text-muted-foreground", children: [
|
|
1792
|
+
"Dictionaries: ",
|
|
1793
|
+
data.dictionaries.join(", ")
|
|
1794
|
+
] })
|
|
1795
|
+
] });
|
|
1796
|
+
}
|
|
1797
|
+
function KeywordsPopoverContent({
|
|
1798
|
+
data
|
|
1799
|
+
}) {
|
|
1800
|
+
const displayLabel = data.label.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1801
|
+
return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
|
|
1802
|
+
/* @__PURE__ */ jsx2(
|
|
1803
|
+
"span",
|
|
1804
|
+
{
|
|
1805
|
+
className: `cat-badge cat-badge-glossary cat-badge-glossary-${data.label}`,
|
|
1806
|
+
children: displayLabel
|
|
1807
|
+
}
|
|
1808
|
+
),
|
|
1809
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
|
|
1810
|
+
"Term:",
|
|
1811
|
+
" ",
|
|
1812
|
+
/* @__PURE__ */ jsx2("strong", { className: "font-semibold text-foreground", children: data.term })
|
|
1813
|
+
] }),
|
|
1814
|
+
data.description && /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground leading-relaxed", children: data.description })
|
|
1815
|
+
] });
|
|
1816
|
+
}
|
|
1817
|
+
function SpecialCharPopoverContent({
|
|
1818
|
+
data
|
|
1819
|
+
}) {
|
|
1820
|
+
const cp = data.char.codePointAt(0) ?? 0;
|
|
1821
|
+
const effectiveMap = getEffectiveCodepointMap();
|
|
1822
|
+
const displaySymbol = effectiveMap[cp] ?? (data.char.trim() === "" ? "\xB7" : data.char);
|
|
1823
|
+
return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-3", children: [
|
|
1824
|
+
/* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-special-char", children: "Special Char" }),
|
|
1825
|
+
/* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx2("span", { className: "inline-flex items-center justify-center min-w-12 min-h-12 rounded-lg border-2 border-border bg-muted px-3 py-2 text-2xl font-bold font-mono text-foreground select-none", children: displaySymbol }) }),
|
|
1826
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground text-center", children: /* @__PURE__ */ jsx2("strong", { className: "font-semibold", children: data.name }) }),
|
|
1827
|
+
/* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center gap-3 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.codePoint }) })
|
|
1828
|
+
] });
|
|
1829
|
+
}
|
|
1830
|
+
function TagPopoverContent({
|
|
1831
|
+
data
|
|
1832
|
+
}) {
|
|
1833
|
+
const isPlaceholder = !data.isClosing && !data.isSelfClosing && data.tagName === data.originalText;
|
|
1834
|
+
return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
|
|
1835
|
+
/* @__PURE__ */ jsxs2("span", { className: "cat-badge cat-badge-tag", children: [
|
|
1836
|
+
isPlaceholder ? "Placeholder" : "Tag",
|
|
1837
|
+
" #",
|
|
1838
|
+
data.tagNumber
|
|
1839
|
+
] }),
|
|
1840
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
|
|
1841
|
+
isPlaceholder ? "Placeholder" : data.isClosing ? "Closing tag" : data.isSelfClosing ? "Self-closing tag" : "Opening tag",
|
|
1842
|
+
":",
|
|
1843
|
+
" ",
|
|
1844
|
+
/* @__PURE__ */ jsx2("strong", { className: "font-semibold text-foreground", children: data.originalText })
|
|
1845
|
+
] }),
|
|
1846
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
|
|
1847
|
+
"Collapsed:",
|
|
1848
|
+
" ",
|
|
1849
|
+
/* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.displayText })
|
|
1850
|
+
] }),
|
|
1851
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground break-all", children: [
|
|
1852
|
+
"Original:",
|
|
1853
|
+
" ",
|
|
1854
|
+
/* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.originalText })
|
|
1855
|
+
] })
|
|
1856
|
+
] });
|
|
1857
|
+
}
|
|
1858
|
+
function LinkPopoverContent({
|
|
1859
|
+
data,
|
|
1860
|
+
onOpen
|
|
1861
|
+
}) {
|
|
1862
|
+
return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
|
|
1863
|
+
/* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-link", children: "Link" }),
|
|
1864
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground break-all", children: /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-xs", children: data.url }) }),
|
|
1865
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "cat-suggestion-btn", onClick: onOpen, children: "Open link \u2197" })
|
|
1866
|
+
] });
|
|
1867
|
+
}
|
|
1868
|
+
function QuotePopoverContent({
|
|
1869
|
+
data
|
|
1870
|
+
}) {
|
|
1871
|
+
return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
|
|
1872
|
+
/* @__PURE__ */ jsx2(
|
|
1873
|
+
"span",
|
|
1874
|
+
{
|
|
1875
|
+
className: `cat-badge cat-badge-quote cat-badge-quote-${data.quoteType}`,
|
|
1876
|
+
children: data.quoteType === "single" ? "Single Quote" : "Double Quote"
|
|
1877
|
+
}
|
|
1878
|
+
),
|
|
1879
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
|
|
1880
|
+
data.position === "opening" ? "Opening" : "Closing",
|
|
1881
|
+
" quote"
|
|
1882
|
+
] }),
|
|
1883
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
1884
|
+
/* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.originalChar }),
|
|
1885
|
+
/* @__PURE__ */ jsx2("span", { children: "\u2192" }),
|
|
1886
|
+
/* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.replacementChar })
|
|
1887
|
+
] })
|
|
1888
|
+
] });
|
|
1889
|
+
}
|
|
1890
|
+
function HighlightPopover({
|
|
1891
|
+
state,
|
|
1892
|
+
annotationMap,
|
|
1893
|
+
onSuggestionClick,
|
|
1894
|
+
onLinkOpen,
|
|
1895
|
+
onDismiss,
|
|
1896
|
+
onPopoverEnter,
|
|
1897
|
+
renderPopoverContent,
|
|
1898
|
+
dir
|
|
1899
|
+
}) {
|
|
1900
|
+
const popoverRef = useRef3(null);
|
|
1901
|
+
const popperRef = useRef3(null);
|
|
1902
|
+
useLayoutEffect(() => {
|
|
1903
|
+
const el = popoverRef.current;
|
|
1904
|
+
if (!el) {
|
|
1905
|
+
popperRef.current?.destroy();
|
|
1906
|
+
popperRef.current = null;
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const ar = state.anchorRect;
|
|
1910
|
+
const virtualEl = {
|
|
1911
|
+
getBoundingClientRect: () => ({
|
|
1912
|
+
top: ar?.top ?? state.y,
|
|
1913
|
+
left: ar?.left ?? state.x,
|
|
1914
|
+
bottom: ar?.bottom ?? state.y,
|
|
1915
|
+
right: ar?.right ?? state.x,
|
|
1916
|
+
width: ar?.width ?? 0,
|
|
1917
|
+
height: ar?.height ?? 0,
|
|
1918
|
+
x: ar?.left ?? state.x,
|
|
1919
|
+
y: ar?.top ?? state.y,
|
|
1920
|
+
toJSON: () => {
|
|
1921
|
+
}
|
|
1922
|
+
})
|
|
1923
|
+
};
|
|
1924
|
+
el.style.visibility = "hidden";
|
|
1925
|
+
if (popperRef.current) {
|
|
1926
|
+
popperRef.current.state.elements.reference = virtualEl;
|
|
1927
|
+
} else {
|
|
1928
|
+
popperRef.current = createPopper(virtualEl, el, {
|
|
1929
|
+
strategy: "fixed",
|
|
1930
|
+
placement: "bottom-start",
|
|
1931
|
+
modifiers: [
|
|
1932
|
+
{ name: "offset", options: { offset: [0, 6] } },
|
|
1933
|
+
{ name: "preventOverflow", options: { padding: 16 } },
|
|
1934
|
+
{ name: "flip", options: { padding: 16 } }
|
|
1935
|
+
]
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
popperRef.current.forceUpdate();
|
|
1939
|
+
el.style.visibility = "";
|
|
1940
|
+
}, [state.visible, state.x, state.y, state.anchorRect]);
|
|
1941
|
+
useLayoutEffect(() => {
|
|
1942
|
+
return () => {
|
|
1943
|
+
popperRef.current?.destroy();
|
|
1944
|
+
popperRef.current = null;
|
|
1945
|
+
};
|
|
1946
|
+
}, []);
|
|
1947
|
+
if (!state.visible) return null;
|
|
1948
|
+
const annotations = state.ruleIds.map((id) => annotationMap.get(id)).filter((a) => a != null);
|
|
1949
|
+
if (annotations.length === 0) return null;
|
|
1950
|
+
return /* @__PURE__ */ jsx2(
|
|
1951
|
+
"div",
|
|
1952
|
+
{
|
|
1953
|
+
ref: popoverRef,
|
|
1954
|
+
className: "cat-popover",
|
|
1955
|
+
dir,
|
|
1956
|
+
style: {
|
|
1957
|
+
position: "fixed",
|
|
1958
|
+
left: 0,
|
|
1959
|
+
top: 0,
|
|
1960
|
+
zIndex: 1e3,
|
|
1961
|
+
visibility: "hidden"
|
|
1962
|
+
},
|
|
1963
|
+
onMouseEnter: () => onPopoverEnter(),
|
|
1964
|
+
onMouseLeave: () => onDismiss(),
|
|
1965
|
+
children: annotations.map((ann, i) => {
|
|
1966
|
+
const custom = renderPopoverContent?.({
|
|
1967
|
+
annotation: ann,
|
|
1968
|
+
onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
|
|
1969
|
+
});
|
|
1970
|
+
return /* @__PURE__ */ jsxs2(React.Fragment, { children: [
|
|
1971
|
+
i > 0 && /* @__PURE__ */ jsx2("hr", { className: "border-border my-0" }),
|
|
1972
|
+
custom != null ? custom : ann.type === "spellcheck" ? /* @__PURE__ */ jsx2(
|
|
1973
|
+
SpellCheckPopoverContent,
|
|
1974
|
+
{
|
|
1975
|
+
data: ann.data,
|
|
1976
|
+
onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
|
|
1977
|
+
}
|
|
1978
|
+
) : ann.type === "glossary" ? /* @__PURE__ */ jsx2(KeywordsPopoverContent, { data: ann.data }) : ann.type === "tag" ? /* @__PURE__ */ jsx2(TagPopoverContent, { data: ann.data }) : ann.type === "quote" ? /* @__PURE__ */ jsx2(QuotePopoverContent, { data: ann.data }) : ann.type === "link" ? /* @__PURE__ */ jsx2(
|
|
1979
|
+
LinkPopoverContent,
|
|
1980
|
+
{
|
|
1981
|
+
data: ann.data,
|
|
1982
|
+
onOpen: () => {
|
|
1983
|
+
if (onLinkOpen) {
|
|
1984
|
+
onLinkOpen(ann.data.url);
|
|
1985
|
+
} else {
|
|
1986
|
+
window.open(ann.data.url, "_blank", "noopener,noreferrer");
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
) : /* @__PURE__ */ jsx2(SpecialCharPopoverContent, { data: ann.data })
|
|
1991
|
+
] }, ann.id);
|
|
1992
|
+
})
|
|
1993
|
+
}
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/layout/cat-editor/CATEditor.tsx
|
|
1998
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1999
|
+
var CATEditor = forwardRef(
|
|
2000
|
+
function CATEditor2({
|
|
2001
|
+
initialText = "",
|
|
2002
|
+
rules = [],
|
|
2003
|
+
onChange,
|
|
2004
|
+
onSuggestionApply,
|
|
2005
|
+
codepointDisplayMap,
|
|
2006
|
+
renderPopoverContent,
|
|
2007
|
+
onLinkClick,
|
|
2008
|
+
openLinksOnClick = true,
|
|
2009
|
+
onMentionClick,
|
|
2010
|
+
onMentionInsert,
|
|
2011
|
+
mentionSerialize,
|
|
2012
|
+
mentionPattern,
|
|
2013
|
+
renderMentionDOM,
|
|
2014
|
+
placeholder = "Start typing or paste text here\u2026",
|
|
2015
|
+
className,
|
|
2016
|
+
dir,
|
|
2017
|
+
popoverDir: popoverDirProp = "ltr",
|
|
2018
|
+
jpFont = false,
|
|
2019
|
+
editable: editableProp,
|
|
2020
|
+
readOnlySelectable = false,
|
|
2021
|
+
onKeyDown: onKeyDownProp,
|
|
2022
|
+
readOnly: readOnlyLegacy = false
|
|
2023
|
+
}, ref) {
|
|
2024
|
+
const isEditable = editableProp !== void 0 ? editableProp : !readOnlyLegacy;
|
|
2025
|
+
const annotationMapRef = useRef4(/* @__PURE__ */ new Map());
|
|
2026
|
+
const editorRef = useRef4(null);
|
|
2027
|
+
const savedSelectionRef = useRef4(null);
|
|
2028
|
+
const containerRef = useRef4(null);
|
|
2029
|
+
const dismissTimerRef = useRef4(null);
|
|
2030
|
+
useEffect3(() => {
|
|
2031
|
+
setMentionNodeConfig({
|
|
2032
|
+
renderDOM: renderMentionDOM,
|
|
2033
|
+
serialize: mentionSerialize,
|
|
2034
|
+
pattern: mentionPattern
|
|
2035
|
+
});
|
|
2036
|
+
}, [renderMentionDOM, mentionSerialize, mentionPattern]);
|
|
2037
|
+
const flashIdRef = useRef4(null);
|
|
2038
|
+
const flashTimerRef = useRef4(null);
|
|
2039
|
+
const flashEditUnregRef = useRef4(null);
|
|
2040
|
+
const applyFlashClass = useCallback3((annotationId) => {
|
|
2041
|
+
const container = containerRef.current;
|
|
2042
|
+
if (!container) return;
|
|
2043
|
+
container.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
|
|
2044
|
+
container.querySelectorAll(".cat-highlight").forEach((el) => {
|
|
2045
|
+
const ids = el.getAttribute("data-rule-ids");
|
|
2046
|
+
if (ids && ids.split(",").includes(annotationId)) {
|
|
2047
|
+
el.classList.add("cat-highlight-flash");
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
}, []);
|
|
2051
|
+
const clearFlashInner = useCallback3(() => {
|
|
2052
|
+
flashIdRef.current = null;
|
|
2053
|
+
if (flashTimerRef.current) {
|
|
2054
|
+
clearTimeout(flashTimerRef.current);
|
|
2055
|
+
flashTimerRef.current = null;
|
|
2056
|
+
}
|
|
2057
|
+
if (flashEditUnregRef.current) {
|
|
2058
|
+
flashEditUnregRef.current();
|
|
2059
|
+
flashEditUnregRef.current = null;
|
|
2060
|
+
}
|
|
2061
|
+
containerRef.current?.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
|
|
2062
|
+
}, []);
|
|
2063
|
+
const [popoverState, setPopoverState] = useState2({
|
|
2064
|
+
visible: false,
|
|
2065
|
+
x: 0,
|
|
2066
|
+
y: 0,
|
|
2067
|
+
ruleIds: []
|
|
2068
|
+
});
|
|
2069
|
+
useImperativeHandle(
|
|
2070
|
+
ref,
|
|
2071
|
+
() => ({
|
|
2072
|
+
insertText: (text) => {
|
|
2073
|
+
const editor = editorRef.current;
|
|
2074
|
+
if (!editor) return;
|
|
2075
|
+
editor.update(() => {
|
|
2076
|
+
const saved = savedSelectionRef.current;
|
|
2077
|
+
if (saved) {
|
|
2078
|
+
const anchorPt = $globalOffsetToPoint(saved.anchor);
|
|
2079
|
+
const focusPt = $globalOffsetToPoint(saved.focus);
|
|
2080
|
+
if (anchorPt && focusPt) {
|
|
2081
|
+
const anchorNode = $getNodeByKey(anchorPt.key);
|
|
2082
|
+
if (anchorNode && $isHighlightNode(anchorNode) && anchorNode.getMode() === "token" && saved.anchor === saved.focus) {
|
|
2083
|
+
const newText = $createTextNode3(text);
|
|
2084
|
+
if (anchorPt.offset === 0 || anchorPt.offset < anchorNode.getTextContentSize()) {
|
|
2085
|
+
anchorNode.insertBefore(newText);
|
|
2086
|
+
} else {
|
|
2087
|
+
anchorNode.insertAfter(newText);
|
|
2088
|
+
}
|
|
2089
|
+
newText.selectEnd();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const sel2 = $createRangeSelection2();
|
|
2093
|
+
sel2.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
|
|
2094
|
+
sel2.focus.set(focusPt.key, focusPt.offset, focusPt.type);
|
|
2095
|
+
$setSelection2(sel2);
|
|
2096
|
+
sel2.insertText(text);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const root = $getRoot3();
|
|
2101
|
+
const lastChild = root.getLastChild();
|
|
2102
|
+
if (lastChild) {
|
|
2103
|
+
lastChild.selectEnd();
|
|
2104
|
+
}
|
|
2105
|
+
const sel = $createRangeSelection2();
|
|
2106
|
+
$setSelection2(sel);
|
|
2107
|
+
sel.insertText(text);
|
|
2108
|
+
});
|
|
2109
|
+
editor.focus();
|
|
2110
|
+
},
|
|
2111
|
+
focus: () => {
|
|
2112
|
+
editorRef.current?.focus();
|
|
2113
|
+
},
|
|
2114
|
+
getText: () => {
|
|
2115
|
+
let text = "";
|
|
2116
|
+
editorRef.current?.getEditorState().read(() => {
|
|
2117
|
+
text = $getRoot3().getTextContent();
|
|
2118
|
+
});
|
|
2119
|
+
return text;
|
|
2120
|
+
},
|
|
2121
|
+
flashHighlight: (annotationId, durationMs = 5e3) => {
|
|
2122
|
+
clearFlashInner();
|
|
2123
|
+
flashIdRef.current = annotationId;
|
|
2124
|
+
applyFlashClass(annotationId);
|
|
2125
|
+
flashTimerRef.current = setTimeout(() => {
|
|
2126
|
+
clearFlashInner();
|
|
2127
|
+
}, durationMs);
|
|
2128
|
+
const editor = editorRef.current;
|
|
2129
|
+
if (editor) {
|
|
2130
|
+
flashEditUnregRef.current = editor.registerUpdateListener(
|
|
2131
|
+
({ tags }) => {
|
|
2132
|
+
if (tags.has("cat-highlights")) {
|
|
2133
|
+
if (flashIdRef.current) {
|
|
2134
|
+
requestAnimationFrame(
|
|
2135
|
+
() => applyFlashClass(flashIdRef.current)
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
clearFlashInner();
|
|
2141
|
+
}
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
replaceAll: (search, replacement) => {
|
|
2146
|
+
const editor = editorRef.current;
|
|
2147
|
+
if (!editor || !search) return 0;
|
|
2148
|
+
let count = 0;
|
|
2149
|
+
editor.update(() => {
|
|
2150
|
+
const root = $getRoot3();
|
|
2151
|
+
const fullText = root.getTextContent();
|
|
2152
|
+
let idx = 0;
|
|
2153
|
+
while ((idx = fullText.indexOf(search, idx)) !== -1) {
|
|
2154
|
+
count++;
|
|
2155
|
+
idx += search.length;
|
|
2156
|
+
}
|
|
2157
|
+
if (count === 0) return;
|
|
2158
|
+
const newText = fullText.split(search).join(replacement);
|
|
2159
|
+
root.clear();
|
|
2160
|
+
const lines = newText.split("\n");
|
|
2161
|
+
for (const line of lines) {
|
|
2162
|
+
const p = $createParagraphNode2();
|
|
2163
|
+
p.append($createTextNode3(line));
|
|
2164
|
+
root.append(p);
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
return count;
|
|
2168
|
+
},
|
|
2169
|
+
clearFlash: () => {
|
|
2170
|
+
clearFlashInner();
|
|
2171
|
+
}
|
|
2172
|
+
}),
|
|
2173
|
+
[applyFlashClass, clearFlashInner]
|
|
2174
|
+
);
|
|
2175
|
+
const initialConfig = useMemo2(
|
|
2176
|
+
() => ({
|
|
2177
|
+
namespace: "CATEditor",
|
|
2178
|
+
theme: {
|
|
2179
|
+
root: "cat-editor-root",
|
|
2180
|
+
paragraph: "cat-editor-paragraph",
|
|
2181
|
+
text: {
|
|
2182
|
+
base: "cat-editor-text"
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
nodes: [HighlightNode, MentionNode],
|
|
2186
|
+
// When readOnlySelectable, Lexical must be editable so the caret
|
|
2187
|
+
// and selection work — we block mutations via KEY_DOWN_COMMAND.
|
|
2188
|
+
editable: isEditable || readOnlySelectable,
|
|
2189
|
+
onError: (error) => {
|
|
2190
|
+
console.error("CATEditor Lexical error:", error);
|
|
2191
|
+
},
|
|
2192
|
+
editorState: () => {
|
|
2193
|
+
const root = $getRoot3();
|
|
2194
|
+
const lines = initialText.split("\n");
|
|
2195
|
+
for (const line of lines) {
|
|
2196
|
+
const p = $createParagraphNode2();
|
|
2197
|
+
p.append($createTextNode3(line));
|
|
2198
|
+
root.append(p);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}),
|
|
2202
|
+
[]
|
|
2203
|
+
// intentional: initialConfig should not change
|
|
2204
|
+
);
|
|
2205
|
+
const isOverHighlightRef = useRef4(false);
|
|
2206
|
+
const isOverPopoverRef = useRef4(false);
|
|
2207
|
+
const scheduleHide = useCallback3(() => {
|
|
2208
|
+
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
2209
|
+
dismissTimerRef.current = setTimeout(() => {
|
|
2210
|
+
if (!isOverHighlightRef.current && !isOverPopoverRef.current) {
|
|
2211
|
+
setPopoverState((prev) => ({ ...prev, visible: false }));
|
|
2212
|
+
}
|
|
2213
|
+
}, 400);
|
|
2214
|
+
}, []);
|
|
2215
|
+
const cancelHide = useCallback3(() => {
|
|
2216
|
+
if (dismissTimerRef.current) {
|
|
2217
|
+
clearTimeout(dismissTimerRef.current);
|
|
2218
|
+
dismissTimerRef.current = null;
|
|
2219
|
+
}
|
|
2220
|
+
}, []);
|
|
2221
|
+
useEffect3(() => {
|
|
2222
|
+
const container = containerRef.current;
|
|
2223
|
+
if (!container) return;
|
|
2224
|
+
const handleMouseOver = (e) => {
|
|
2225
|
+
const target = e.target.closest(".cat-highlight");
|
|
2226
|
+
if (!target) {
|
|
2227
|
+
if (isOverHighlightRef.current) {
|
|
2228
|
+
isOverHighlightRef.current = false;
|
|
2229
|
+
scheduleHide();
|
|
2230
|
+
}
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const ruleIdsAttr = target.getAttribute("data-rule-ids");
|
|
2234
|
+
if (!ruleIdsAttr) return;
|
|
2235
|
+
const ruleIds = [
|
|
2236
|
+
...new Set(
|
|
2237
|
+
ruleIdsAttr.split(",").map(
|
|
2238
|
+
(id) => id.startsWith(NL_MARKER_PREFIX) ? id.slice(NL_MARKER_PREFIX.length) : id
|
|
2239
|
+
)
|
|
2240
|
+
)
|
|
2241
|
+
];
|
|
2242
|
+
isOverHighlightRef.current = true;
|
|
2243
|
+
cancelHide();
|
|
2244
|
+
const rect = target.getBoundingClientRect();
|
|
2245
|
+
setPopoverState({
|
|
2246
|
+
visible: true,
|
|
2247
|
+
x: rect.left,
|
|
2248
|
+
y: rect.bottom,
|
|
2249
|
+
anchorRect: {
|
|
2250
|
+
top: rect.top,
|
|
2251
|
+
left: rect.left,
|
|
2252
|
+
bottom: rect.bottom,
|
|
2253
|
+
right: rect.right,
|
|
2254
|
+
width: rect.width,
|
|
2255
|
+
height: rect.height
|
|
2256
|
+
},
|
|
2257
|
+
ruleIds
|
|
2258
|
+
});
|
|
2259
|
+
};
|
|
2260
|
+
const handleMouseOut = (e) => {
|
|
2261
|
+
const related = e.relatedTarget;
|
|
2262
|
+
if (related?.closest(".cat-highlight")) return;
|
|
2263
|
+
isOverHighlightRef.current = false;
|
|
2264
|
+
scheduleHide();
|
|
2265
|
+
};
|
|
2266
|
+
container.addEventListener("mouseover", handleMouseOver);
|
|
2267
|
+
container.addEventListener("mouseout", handleMouseOut);
|
|
2268
|
+
return () => {
|
|
2269
|
+
container.removeEventListener("mouseover", handleMouseOver);
|
|
2270
|
+
container.removeEventListener("mouseout", handleMouseOut);
|
|
2271
|
+
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
2272
|
+
};
|
|
2273
|
+
}, [scheduleHide, cancelHide]);
|
|
2274
|
+
useEffect3(() => {
|
|
2275
|
+
const container = containerRef.current;
|
|
2276
|
+
if (!container) return;
|
|
2277
|
+
const handleClick = (e) => {
|
|
2278
|
+
if (openLinksOnClick) {
|
|
2279
|
+
const highlightTarget = e.target.closest(
|
|
2280
|
+
".cat-highlight"
|
|
2281
|
+
);
|
|
2282
|
+
if (highlightTarget) {
|
|
2283
|
+
const ruleIdsAttr = highlightTarget.getAttribute("data-rule-ids");
|
|
2284
|
+
if (ruleIdsAttr) {
|
|
2285
|
+
const ids = ruleIdsAttr.split(",");
|
|
2286
|
+
for (const id of ids) {
|
|
2287
|
+
const ann = annotationMapRef.current.get(id);
|
|
2288
|
+
if (!ann) continue;
|
|
2289
|
+
if (ann.type === "link") {
|
|
2290
|
+
e.preventDefault();
|
|
2291
|
+
if (onLinkClick) {
|
|
2292
|
+
onLinkClick(ann.data.url);
|
|
2293
|
+
} else {
|
|
2294
|
+
window.open(ann.data.url, "_blank", "noopener,noreferrer");
|
|
2295
|
+
}
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const mentionTarget = e.target.closest(
|
|
2303
|
+
".cat-mention-node"
|
|
2304
|
+
);
|
|
2305
|
+
if (mentionTarget) {
|
|
2306
|
+
const mentionId = mentionTarget.getAttribute("data-mention-id");
|
|
2307
|
+
const mentionName = mentionTarget.getAttribute("data-mention-name");
|
|
2308
|
+
if (mentionId && mentionName) {
|
|
2309
|
+
e.preventDefault();
|
|
2310
|
+
onMentionClick?.(mentionId, mentionName);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
container.addEventListener("click", handleClick);
|
|
2316
|
+
return () => {
|
|
2317
|
+
container.removeEventListener("click", handleClick);
|
|
2318
|
+
};
|
|
2319
|
+
}, [onLinkClick, openLinksOnClick, onMentionClick]);
|
|
2320
|
+
const handleSuggestionClick = useCallback3(
|
|
2321
|
+
(suggestion, ruleId) => {
|
|
2322
|
+
const editor = editorRef.current;
|
|
2323
|
+
if (!editor) return;
|
|
2324
|
+
let replacedRange;
|
|
2325
|
+
editor.update(() => {
|
|
2326
|
+
const root = $getRoot3();
|
|
2327
|
+
const allNodes = root.getAllTextNodes();
|
|
2328
|
+
for (const node of allNodes) {
|
|
2329
|
+
if ($isHighlightNode(node) && node.__ruleIds.split(",").includes(ruleId)) {
|
|
2330
|
+
const globalOffset = $pointToGlobalOffset(node.getKey(), 0);
|
|
2331
|
+
const originalContent = node.getTextContent();
|
|
2332
|
+
replacedRange = {
|
|
2333
|
+
start: globalOffset,
|
|
2334
|
+
end: globalOffset + originalContent.length,
|
|
2335
|
+
content: originalContent
|
|
2336
|
+
};
|
|
2337
|
+
const textNode = $createTextNode3(suggestion);
|
|
2338
|
+
node.replace(textNode);
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
setPopoverState((prev) => ({ ...prev, visible: false }));
|
|
2344
|
+
if (replacedRange !== void 0) {
|
|
2345
|
+
const annotation = annotationMapRef.current.get(ruleId);
|
|
2346
|
+
if (annotation) {
|
|
2347
|
+
onSuggestionApply?.(
|
|
2348
|
+
ruleId,
|
|
2349
|
+
suggestion,
|
|
2350
|
+
replacedRange,
|
|
2351
|
+
annotation.type
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
},
|
|
2356
|
+
[onSuggestionApply]
|
|
2357
|
+
);
|
|
2358
|
+
const handleChange = useCallback3(
|
|
2359
|
+
(editorState) => {
|
|
2360
|
+
if (!onChange) return;
|
|
2361
|
+
editorState.read(() => {
|
|
2362
|
+
const root = $getRoot3();
|
|
2363
|
+
onChange(root.getTextContent());
|
|
2364
|
+
});
|
|
2365
|
+
},
|
|
2366
|
+
[onChange]
|
|
2367
|
+
);
|
|
2368
|
+
return /* @__PURE__ */ jsxs3(
|
|
2369
|
+
"div",
|
|
2370
|
+
{
|
|
2371
|
+
ref: containerRef,
|
|
2372
|
+
className: cn(
|
|
2373
|
+
"cat-editor-container",
|
|
2374
|
+
jpFont && "cat-editor-jp-font",
|
|
2375
|
+
className
|
|
2376
|
+
),
|
|
2377
|
+
dir,
|
|
2378
|
+
children: [
|
|
2379
|
+
/* @__PURE__ */ jsx3(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxs3("div", { className: "cat-editor-inner", children: [
|
|
2380
|
+
/* @__PURE__ */ jsx3(
|
|
2381
|
+
PlainTextPlugin,
|
|
2382
|
+
{
|
|
2383
|
+
contentEditable: /* @__PURE__ */ jsx3(
|
|
2384
|
+
ContentEditable,
|
|
2385
|
+
{
|
|
2386
|
+
className: cn(
|
|
2387
|
+
"cat-editor-editable",
|
|
2388
|
+
!isEditable && !readOnlySelectable && "cat-editor-readonly",
|
|
2389
|
+
!isEditable && readOnlySelectable && "cat-editor-readonly-selectable"
|
|
2390
|
+
)
|
|
2391
|
+
}
|
|
2392
|
+
),
|
|
2393
|
+
placeholder: /* @__PURE__ */ jsx3("div", { className: "cat-editor-placeholder", children: placeholder }),
|
|
2394
|
+
ErrorBoundary: LexicalErrorBoundary
|
|
2395
|
+
}
|
|
2396
|
+
),
|
|
2397
|
+
/* @__PURE__ */ jsx3(HistoryPlugin, {}),
|
|
2398
|
+
/* @__PURE__ */ jsx3(OnChangePlugin, { onChange: handleChange }),
|
|
2399
|
+
/* @__PURE__ */ jsx3(
|
|
2400
|
+
HighlightsPlugin,
|
|
2401
|
+
{
|
|
2402
|
+
rules,
|
|
2403
|
+
annotationMapRef,
|
|
2404
|
+
codepointDisplayMap
|
|
2405
|
+
}
|
|
2406
|
+
),
|
|
2407
|
+
/* @__PURE__ */ jsx3(
|
|
2408
|
+
EditorRefPlugin,
|
|
2409
|
+
{
|
|
2410
|
+
editorRef,
|
|
2411
|
+
savedSelectionRef
|
|
2412
|
+
}
|
|
2413
|
+
),
|
|
2414
|
+
/* @__PURE__ */ jsx3(NLMarkerNavigationPlugin, {}),
|
|
2415
|
+
!isEditable && readOnlySelectable && /* @__PURE__ */ jsx3(ReadOnlySelectablePlugin, {}),
|
|
2416
|
+
onKeyDownProp && /* @__PURE__ */ jsx3(KeyDownPlugin, { onKeyDown: onKeyDownProp }),
|
|
2417
|
+
dir && dir !== "auto" && /* @__PURE__ */ jsx3(DirectionPlugin, { dir }),
|
|
2418
|
+
rules.filter((r) => r.type === "mention").map((mentionRule, i) => /* @__PURE__ */ jsx3(
|
|
2419
|
+
MentionPlugin,
|
|
2420
|
+
{
|
|
2421
|
+
users: mentionRule.users,
|
|
2422
|
+
trigger: mentionRule.trigger,
|
|
2423
|
+
onMentionInsert
|
|
2424
|
+
},
|
|
2425
|
+
`mention-${i}`
|
|
2426
|
+
))
|
|
2427
|
+
] }) }),
|
|
2428
|
+
/* @__PURE__ */ jsx3(
|
|
2429
|
+
HighlightPopover,
|
|
2430
|
+
{
|
|
2431
|
+
state: popoverState,
|
|
2432
|
+
annotationMap: annotationMapRef.current,
|
|
2433
|
+
onSuggestionClick: handleSuggestionClick,
|
|
2434
|
+
onLinkOpen: onLinkClick,
|
|
2435
|
+
onDismiss: () => {
|
|
2436
|
+
isOverPopoverRef.current = false;
|
|
2437
|
+
scheduleHide();
|
|
2438
|
+
},
|
|
2439
|
+
onPopoverEnter: () => {
|
|
2440
|
+
isOverPopoverRef.current = true;
|
|
2441
|
+
cancelHide();
|
|
2442
|
+
},
|
|
2443
|
+
renderPopoverContent,
|
|
2444
|
+
dir: popoverDirProp === "inherit" ? dir : popoverDirProp
|
|
2445
|
+
}
|
|
2446
|
+
)
|
|
2447
|
+
]
|
|
2448
|
+
}
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
);
|
|
2452
|
+
var CATEditor_default = CATEditor;
|
|
2453
|
+
function ReadOnlySelectablePlugin() {
|
|
2454
|
+
const [editor] = useLexicalComposerContext3();
|
|
2455
|
+
useEffect3(() => {
|
|
2456
|
+
return editor.registerCommand(
|
|
2457
|
+
KEY_DOWN_COMMAND,
|
|
2458
|
+
(event) => {
|
|
2459
|
+
if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End" || event.key === "PageUp" || event.key === "PageDown" || event.key === "Shift" || event.key === "Control" || event.key === "Alt" || event.key === "Meta" || event.key === "Tab" || event.key === "Escape" || event.key === "F5" || event.key === "F12" || // Ctrl/Cmd shortcuts that don't mutate: copy, select-all, find
|
|
2460
|
+
(event.ctrlKey || event.metaKey) && (event.key === "c" || event.key === "a" || event.key === "f" || event.key === "g")) {
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
event.preventDefault();
|
|
2464
|
+
return true;
|
|
2465
|
+
},
|
|
2466
|
+
COMMAND_PRIORITY_CRITICAL
|
|
2467
|
+
);
|
|
2468
|
+
}, [editor]);
|
|
2469
|
+
useEffect3(() => {
|
|
2470
|
+
const root = editor.getRootElement();
|
|
2471
|
+
if (!root) return;
|
|
2472
|
+
const block = (e) => e.preventDefault();
|
|
2473
|
+
root.addEventListener("paste", block);
|
|
2474
|
+
root.addEventListener("cut", block);
|
|
2475
|
+
root.addEventListener("drop", block);
|
|
2476
|
+
return () => {
|
|
2477
|
+
root.removeEventListener("paste", block);
|
|
2478
|
+
root.removeEventListener("cut", block);
|
|
2479
|
+
root.removeEventListener("drop", block);
|
|
2480
|
+
};
|
|
2481
|
+
}, [editor]);
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
function KeyDownPlugin({
|
|
2485
|
+
onKeyDown
|
|
2486
|
+
}) {
|
|
2487
|
+
const [editor] = useLexicalComposerContext3();
|
|
2488
|
+
useEffect3(() => {
|
|
2489
|
+
return editor.registerCommand(
|
|
2490
|
+
KEY_DOWN_COMMAND,
|
|
2491
|
+
(event) => {
|
|
2492
|
+
return onKeyDown(event);
|
|
2493
|
+
},
|
|
2494
|
+
COMMAND_PRIORITY_CRITICAL
|
|
2495
|
+
);
|
|
2496
|
+
}, [editor, onKeyDown]);
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
function DirectionPlugin({ dir }) {
|
|
2500
|
+
const [editor] = useLexicalComposerContext3();
|
|
2501
|
+
useEffect3(() => {
|
|
2502
|
+
editor.update(() => {
|
|
2503
|
+
$getRoot3().setDirection(dir);
|
|
2504
|
+
});
|
|
2505
|
+
return () => {
|
|
2506
|
+
editor.update(() => {
|
|
2507
|
+
$getRoot3().setDirection(null);
|
|
2508
|
+
});
|
|
2509
|
+
};
|
|
2510
|
+
}, [editor, dir]);
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
export {
|
|
2515
|
+
CODEPOINT_DISPLAY_MAP,
|
|
2516
|
+
getEffectiveCodepointMap,
|
|
2517
|
+
setMentionNodeConfig,
|
|
2518
|
+
getMentionModelText,
|
|
2519
|
+
getMentionPattern,
|
|
2520
|
+
MentionNode,
|
|
2521
|
+
$createMentionNode,
|
|
2522
|
+
$isMentionNode,
|
|
2523
|
+
MentionPlugin,
|
|
2524
|
+
CATEditor,
|
|
2525
|
+
CATEditor_default
|
|
2526
|
+
};
|
|
2527
|
+
//# sourceMappingURL=chunk-ZME2TTK5.js.map
|