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