@longd/layout-ui 0.1.1 → 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 -6
  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
package/dist/index.cjs CHANGED
@@ -30,8 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ BUILTIN_ESCAPE_PATTERNS: () => BUILTIN_ESCAPE_PATTERNS,
34
+ CATEditor: () => CATEditor,
33
35
  LayoutSelect: () => LayoutSelect,
34
- LayoutSelectDefault: () => select_default
36
+ LayoutSelectDefault: () => select_default,
37
+ detectQuotes: () => detectQuotes
35
38
  });
36
39
  module.exports = __toCommonJS(src_exports);
37
40
 
@@ -352,8 +355,9 @@ function MultipleTriggerContent({
352
355
  let maxVisible = collapsed ? value.length : visibleCount;
353
356
  const hasExplicitLimit = !collapsed && showItemsLength !== void 0;
354
357
  if (hasExplicitLimit) {
355
- maxVisible = showItemsLength;
358
+ maxVisible = Math.min(visibleCount, showItemsLength);
356
359
  }
360
+ maxVisible = Math.max(1, maxVisible);
357
361
  const hasOverflow = maxVisible < value.length;
358
362
  const displayed = value.slice(0, maxVisible);
359
363
  const overflowItems = value.slice(maxVisible);
@@ -1090,9 +1094,2614 @@ function LayoutSelect(props) {
1090
1094
  ] }) });
1091
1095
  }
1092
1096
  var select_default = LayoutSelect;
1097
+
1098
+ // src/layout/cat-editor/CATEditor.tsx
1099
+ var import_react5 = require("react");
1100
+ var import_lexical6 = require("lexical");
1101
+ var import_LexicalComposer = require("@lexical/react/LexicalComposer");
1102
+ var import_LexicalComposerContext3 = require("@lexical/react/LexicalComposerContext");
1103
+ var import_LexicalContentEditable = require("@lexical/react/LexicalContentEditable");
1104
+ var import_LexicalErrorBoundary = require("@lexical/react/LexicalErrorBoundary");
1105
+ var import_LexicalHistoryPlugin = require("@lexical/react/LexicalHistoryPlugin");
1106
+ var import_LexicalOnChangePlugin = require("@lexical/react/LexicalOnChangePlugin");
1107
+ var import_LexicalPlainTextPlugin = require("@lexical/react/LexicalPlainTextPlugin");
1108
+
1109
+ // src/layout/cat-editor/constants.ts
1110
+ var CODEPOINT_DISPLAY_MAP = {
1111
+ 0: "\u2400",
1112
+ 9: "\u21E5",
1113
+ 10: "\u21A9",
1114
+ 12: "\u240C",
1115
+ 13: "\u21B5",
1116
+ 160: "\u237D",
1117
+ 8194: "\u2423",
1118
+ 8195: "\u2423",
1119
+ 8201: "\xB7",
1120
+ 8202: "\xB7",
1121
+ 8203: "\u2205",
1122
+ 8204: "\u2298",
1123
+ 8205: "\u2295",
1124
+ 8288: "\u2040",
1125
+ 12288: "\u25A1",
1126
+ 65279: "\u25CA"
1127
+ };
1128
+ var _codepointOverrides;
1129
+ function setCodepointOverrides(overrides) {
1130
+ _codepointOverrides = overrides;
1131
+ }
1132
+ function getEffectiveCodepointMap() {
1133
+ return _codepointOverrides ? { ...CODEPOINT_DISPLAY_MAP, ..._codepointOverrides } : CODEPOINT_DISPLAY_MAP;
1134
+ }
1135
+ var NL_MARKER_PREFIX = "__nl-";
1136
+ function replaceInvisibleChars(text, overrides) {
1137
+ const map = overrides ? { ...CODEPOINT_DISPLAY_MAP, ...overrides } : getEffectiveCodepointMap();
1138
+ let result = "";
1139
+ for (const ch of text) {
1140
+ const cp = ch.codePointAt(0) ?? 0;
1141
+ result += map[cp] ?? ch;
1142
+ }
1143
+ return result;
1144
+ }
1145
+
1146
+ // src/layout/cat-editor/highlight-node.ts
1147
+ var import_lexical = require("lexical");
1148
+ var HighlightNode = class _HighlightNode extends import_lexical.TextNode {
1149
+ __highlightTypes;
1150
+ __ruleIds;
1151
+ __displayText;
1152
+ static getType() {
1153
+ return "highlight";
1154
+ }
1155
+ static clone(node) {
1156
+ return new _HighlightNode(
1157
+ node.__text,
1158
+ node.__highlightTypes,
1159
+ node.__ruleIds,
1160
+ node.__displayText,
1161
+ node.__key
1162
+ );
1163
+ }
1164
+ constructor(text, highlightTypes, ruleIds, displayText, key) {
1165
+ super(text, key);
1166
+ this.__highlightTypes = highlightTypes;
1167
+ this.__ruleIds = ruleIds;
1168
+ this.__displayText = displayText ?? "";
1169
+ }
1170
+ createDOM(config) {
1171
+ const dom = super.createDOM(config);
1172
+ dom.classList.add("cat-highlight");
1173
+ for (const t of this.__highlightTypes.split(",")) {
1174
+ dom.classList.add(`cat-highlight-${t}`);
1175
+ if (t.startsWith("glossary-")) {
1176
+ dom.classList.add("cat-highlight-glossary");
1177
+ }
1178
+ if (t.startsWith("spellcheck-")) {
1179
+ dom.classList.add("cat-highlight-spellcheck");
1180
+ }
1181
+ }
1182
+ if (this.__highlightTypes.includes(",")) {
1183
+ dom.classList.add("cat-highlight-nested");
1184
+ }
1185
+ dom.dataset.highlightTypes = this.__highlightTypes;
1186
+ dom.dataset.ruleIds = this.__ruleIds;
1187
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
1188
+ dom.style.userSelect = "none";
1189
+ dom.classList.add("cat-highlight-nl-marker");
1190
+ }
1191
+ if (this.__displayText) {
1192
+ dom.dataset.display = this.__displayText;
1193
+ }
1194
+ if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
1195
+ dom.textContent = "\u200B";
1196
+ dom.contentEditable = "false";
1197
+ }
1198
+ if (this.__highlightTypes.split(",").includes("special-char")) {
1199
+ if (this.__text === " ") {
1200
+ dom.classList.add("cat-highlight-space-char");
1201
+ dom.style.position = "relative";
1202
+ } else {
1203
+ const replaced = replaceInvisibleChars(this.__text);
1204
+ if (replaced !== this.__text) {
1205
+ dom.textContent = replaced;
1206
+ }
1207
+ }
1208
+ }
1209
+ if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
1210
+ dom.classList.add("cat-highlight-quote-char");
1211
+ dom.textContent = "\u200B";
1212
+ dom.contentEditable = "false";
1213
+ }
1214
+ return dom;
1215
+ }
1216
+ updateDOM(prevNode, dom, config) {
1217
+ const updated = super.updateDOM(prevNode, dom, config);
1218
+ if (prevNode.__highlightTypes !== this.__highlightTypes) {
1219
+ for (const t of prevNode.__highlightTypes.split(",")) {
1220
+ dom.classList.remove(`cat-highlight-${t}`);
1221
+ if (t.startsWith("glossary-")) {
1222
+ dom.classList.remove("cat-highlight-glossary");
1223
+ }
1224
+ if (t.startsWith("spellcheck-")) {
1225
+ dom.classList.remove("cat-highlight-spellcheck");
1226
+ }
1227
+ }
1228
+ dom.classList.remove("cat-highlight-nested");
1229
+ for (const t of this.__highlightTypes.split(",")) {
1230
+ dom.classList.add(`cat-highlight-${t}`);
1231
+ if (t.startsWith("glossary-")) {
1232
+ dom.classList.add("cat-highlight-glossary");
1233
+ }
1234
+ if (t.startsWith("spellcheck-")) {
1235
+ dom.classList.add("cat-highlight-spellcheck");
1236
+ }
1237
+ }
1238
+ if (this.__highlightTypes.includes(",")) {
1239
+ dom.classList.add("cat-highlight-nested");
1240
+ }
1241
+ dom.dataset.highlightTypes = this.__highlightTypes;
1242
+ }
1243
+ if (prevNode.__ruleIds !== this.__ruleIds) {
1244
+ dom.dataset.ruleIds = this.__ruleIds;
1245
+ }
1246
+ if (prevNode.__displayText !== this.__displayText) {
1247
+ if (this.__displayText) {
1248
+ dom.dataset.display = this.__displayText;
1249
+ } else {
1250
+ delete dom.dataset.display;
1251
+ }
1252
+ }
1253
+ if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
1254
+ dom.textContent = "\u200B";
1255
+ dom.contentEditable = "false";
1256
+ } else if (dom.contentEditable === "false") {
1257
+ dom.removeAttribute("contenteditable");
1258
+ }
1259
+ if (this.__highlightTypes.split(",").includes("special-char")) {
1260
+ if (this.__text === " ") {
1261
+ dom.classList.add("cat-highlight-space-char");
1262
+ dom.style.position = "relative";
1263
+ } else {
1264
+ const replaced = replaceInvisibleChars(this.__text);
1265
+ if (replaced !== this.__text) {
1266
+ dom.textContent = replaced;
1267
+ }
1268
+ }
1269
+ }
1270
+ if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
1271
+ dom.classList.add("cat-highlight-quote-char");
1272
+ dom.textContent = "\u200B";
1273
+ dom.contentEditable = "false";
1274
+ } else if (prevNode.__highlightTypes.split(",").includes("quote")) {
1275
+ dom.classList.remove("cat-highlight-quote-char");
1276
+ if (dom.contentEditable === "false") {
1277
+ dom.removeAttribute("contenteditable");
1278
+ }
1279
+ }
1280
+ return updated;
1281
+ }
1282
+ static importJSON(json) {
1283
+ const node = new _HighlightNode(
1284
+ json.text,
1285
+ json.highlightTypes,
1286
+ json.ruleIds,
1287
+ json.displayText
1288
+ );
1289
+ node.setFormat(json.format);
1290
+ node.setDetail(json.detail);
1291
+ node.setMode(json.mode);
1292
+ node.setStyle(json.style);
1293
+ return node;
1294
+ }
1295
+ exportJSON() {
1296
+ return {
1297
+ ...super.exportJSON(),
1298
+ type: "highlight",
1299
+ highlightTypes: this.__highlightTypes,
1300
+ ruleIds: this.__ruleIds,
1301
+ displayText: this.__displayText
1302
+ };
1303
+ }
1304
+ /** NL-marker nodes must not leak the display symbol ↩ into
1305
+ * clipboard or getTextContent() calls — they are purely visual. */
1306
+ getTextContent() {
1307
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return "";
1308
+ return super.getTextContent();
1309
+ }
1310
+ canInsertTextBefore() {
1311
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
1312
+ if (this.getMode() === "token") return false;
1313
+ return true;
1314
+ }
1315
+ canInsertTextAfter() {
1316
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
1317
+ if (this.getMode() === "token") return false;
1318
+ return true;
1319
+ }
1320
+ isTextEntity() {
1321
+ return false;
1322
+ }
1323
+ };
1324
+ function $createHighlightNode(text, highlightTypes, ruleIds, displayText, forceToken) {
1325
+ const node = new HighlightNode(text, highlightTypes, ruleIds, displayText);
1326
+ if (highlightTypes.split(",").includes("special-char") || ruleIds.startsWith(NL_MARKER_PREFIX) || forceToken) {
1327
+ node.setMode("token");
1328
+ }
1329
+ return node;
1330
+ }
1331
+ function $isHighlightNode(node) {
1332
+ return node instanceof HighlightNode;
1333
+ }
1334
+
1335
+ // src/layout/cat-editor/mention-node.ts
1336
+ var import_lexical2 = require("lexical");
1337
+ var DEFAULT_MENTION_SERIALIZE = (id) => `@{${id}}`;
1338
+ var DEFAULT_MENTION_PATTERN = /@\{([^}]+)\}/g;
1339
+ var _mentionNodeConfig = {};
1340
+ function setMentionNodeConfig(config) {
1341
+ _mentionNodeConfig = config;
1342
+ }
1343
+ function getMentionModelText(id) {
1344
+ return (_mentionNodeConfig.serialize ?? DEFAULT_MENTION_SERIALIZE)(id);
1345
+ }
1346
+ function getMentionPattern() {
1347
+ const src = _mentionNodeConfig.pattern ?? DEFAULT_MENTION_PATTERN;
1348
+ return new RegExp(src.source, src.flags);
1349
+ }
1350
+ function renderDefaultMentionDOM(element, _mentionId, mentionName) {
1351
+ element.textContent = "";
1352
+ const label = document.createElement("span");
1353
+ label.className = "cat-mention-label";
1354
+ label.textContent = `@${mentionName}`;
1355
+ element.appendChild(label);
1356
+ }
1357
+ function $convertMentionElement(domNode) {
1358
+ const mentionId = domNode.getAttribute("data-mention-id");
1359
+ const mentionName = domNode.getAttribute("data-mention-name");
1360
+ if (mentionId !== null) {
1361
+ const node = $createMentionNode(mentionId, mentionName ?? mentionId);
1362
+ return { node };
1363
+ }
1364
+ return null;
1365
+ }
1366
+ var MentionNode = class _MentionNode extends import_lexical2.TextNode {
1367
+ __mentionId;
1368
+ __mentionName;
1369
+ static getType() {
1370
+ return "mention";
1371
+ }
1372
+ static clone(node) {
1373
+ return new _MentionNode(
1374
+ node.__mentionId,
1375
+ node.__mentionName,
1376
+ node.__text,
1377
+ node.__key
1378
+ );
1379
+ }
1380
+ static importJSON(serializedNode) {
1381
+ return $createMentionNode(
1382
+ serializedNode.mentionId,
1383
+ serializedNode.mentionName
1384
+ ).updateFromJSON(serializedNode);
1385
+ }
1386
+ constructor(mentionId, mentionName, text, key) {
1387
+ super(text ?? getMentionModelText(mentionId), key);
1388
+ this.__mentionId = mentionId;
1389
+ this.__mentionName = mentionName;
1390
+ }
1391
+ exportJSON() {
1392
+ return {
1393
+ ...super.exportJSON(),
1394
+ mentionId: this.__mentionId,
1395
+ mentionName: this.__mentionName
1396
+ };
1397
+ }
1398
+ createDOM(config) {
1399
+ const dom = super.createDOM(config);
1400
+ dom.className = "cat-mention-node";
1401
+ dom.spellcheck = false;
1402
+ dom.contentEditable = "false";
1403
+ dom.setAttribute("data-mention-id", this.__mentionId);
1404
+ dom.setAttribute("data-mention-name", this.__mentionName);
1405
+ this._renderInnerDOM(dom);
1406
+ return dom;
1407
+ }
1408
+ updateDOM(prevNode, dom, _config) {
1409
+ if (prevNode.__mentionId !== this.__mentionId || prevNode.__mentionName !== this.__mentionName) {
1410
+ dom.setAttribute("data-mention-id", this.__mentionId);
1411
+ dom.setAttribute("data-mention-name", this.__mentionName);
1412
+ this._renderInnerDOM(dom);
1413
+ }
1414
+ return false;
1415
+ }
1416
+ /** Fill the span with visible content (default: @label, or custom). */
1417
+ _renderInnerDOM(element) {
1418
+ const renderer = _mentionNodeConfig.renderDOM;
1419
+ if (renderer) {
1420
+ const handled = renderer(element, this.__mentionId, this.__mentionName);
1421
+ if (handled) return;
1422
+ }
1423
+ renderDefaultMentionDOM(element, this.__mentionId, this.__mentionName);
1424
+ }
1425
+ exportDOM() {
1426
+ const element = document.createElement("span");
1427
+ element.setAttribute("data-mention", "true");
1428
+ element.setAttribute("data-mention-id", this.__mentionId);
1429
+ element.setAttribute("data-mention-name", this.__mentionName);
1430
+ element.textContent = this.__text;
1431
+ return { element };
1432
+ }
1433
+ static importDOM() {
1434
+ return {
1435
+ span: (domNode) => {
1436
+ if (!domNode.hasAttribute("data-mention")) {
1437
+ return null;
1438
+ }
1439
+ return {
1440
+ conversion: $convertMentionElement,
1441
+ priority: 1
1442
+ };
1443
+ }
1444
+ };
1445
+ }
1446
+ isTextEntity() {
1447
+ return true;
1448
+ }
1449
+ canInsertTextBefore() {
1450
+ return false;
1451
+ }
1452
+ canInsertTextAfter() {
1453
+ return false;
1454
+ }
1455
+ };
1456
+ function $createMentionNode(mentionId, mentionName, textContent) {
1457
+ const node = new MentionNode(
1458
+ mentionId,
1459
+ mentionName,
1460
+ textContent ?? getMentionModelText(mentionId)
1461
+ );
1462
+ node.setMode("token").toggleDirectionless();
1463
+ return (0, import_lexical2.$applyNodeReplacement)(node);
1464
+ }
1465
+ function $isMentionNode(node) {
1466
+ return node instanceof MentionNode;
1467
+ }
1468
+
1469
+ // src/layout/cat-editor/mention-plugin.tsx
1470
+ var import_react2 = require("react");
1471
+ var ReactDOM = __toESM(require("react-dom"), 1);
1472
+ var import_LexicalComposerContext = require("@lexical/react/LexicalComposerContext");
1473
+ var import_LexicalTypeaheadMenuPlugin = require("@lexical/react/LexicalTypeaheadMenuPlugin");
1474
+ var import_react_virtual2 = require("@tanstack/react-virtual");
1475
+ var import_lexical3 = require("lexical");
1476
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1477
+ var SUGGESTION_LIST_LENGTH_LIMIT = 50;
1478
+ var ITEM_HEIGHT = 36;
1479
+ var MentionTypeaheadOption = class extends import_LexicalTypeaheadMenuPlugin.MenuOption {
1480
+ user;
1481
+ constructor(user) {
1482
+ super(user.id);
1483
+ this.user = user;
1484
+ }
1485
+ };
1486
+ function MentionAvatar({
1487
+ user,
1488
+ size = 24
1489
+ }) {
1490
+ if (user.avatar) {
1491
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1492
+ "span",
1493
+ {
1494
+ className: "inline-flex items-center justify-center",
1495
+ style: { width: size, height: size },
1496
+ children: user.avatar()
1497
+ }
1498
+ );
1499
+ }
1500
+ const initials = user.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
1501
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1502
+ "span",
1503
+ {
1504
+ className: "inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium",
1505
+ style: { width: size, height: size, fontSize: size * 0.4 },
1506
+ children: initials
1507
+ }
1508
+ );
1509
+ }
1510
+ function MentionMenuItem({
1511
+ option,
1512
+ isSelected: isSelected2,
1513
+ onClick,
1514
+ onMouseEnter
1515
+ }) {
1516
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1517
+ "li",
1518
+ {
1519
+ tabIndex: -1,
1520
+ className: `flex items-center gap-2 px-2 py-1.5 text-sm cursor-default select-none rounded-sm ${isSelected2 ? "bg-accent text-accent-foreground" : "text-popover-foreground"}`,
1521
+ ref: option.setRefElement,
1522
+ role: "option",
1523
+ "aria-selected": isSelected2,
1524
+ onMouseEnter,
1525
+ onClick,
1526
+ children: [
1527
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MentionAvatar, { user: option.user, size: 22 }),
1528
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate", children: option.user.name })
1529
+ ]
1530
+ },
1531
+ option.key
1532
+ );
1533
+ }
1534
+ function VirtualizedMentionMenu({
1535
+ options,
1536
+ selectedIndex,
1537
+ selectOptionAndCleanUp,
1538
+ setHighlightedIndex
1539
+ }) {
1540
+ const parentRef = (0, import_react2.useRef)(null);
1541
+ const virtualizer = (0, import_react_virtual2.useVirtualizer)({
1542
+ count: options.length,
1543
+ getScrollElement: () => parentRef.current,
1544
+ estimateSize: () => ITEM_HEIGHT,
1545
+ overscan: 8
1546
+ });
1547
+ (0, import_react2.useEffect)(() => {
1548
+ if (selectedIndex !== null && selectedIndex >= 0) {
1549
+ virtualizer.scrollToIndex(selectedIndex, { align: "auto" });
1550
+ }
1551
+ }, [selectedIndex, virtualizer]);
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1553
+ "div",
1554
+ {
1555
+ ref: parentRef,
1556
+ className: "max-h-[280px] overflow-y-auto overflow-x-hidden",
1557
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1558
+ "ul",
1559
+ {
1560
+ role: "listbox",
1561
+ "aria-label": "Mention suggestions",
1562
+ style: {
1563
+ height: `${virtualizer.getTotalSize()}px`,
1564
+ position: "relative",
1565
+ width: "100%"
1566
+ },
1567
+ children: virtualizer.getVirtualItems().map((vItem) => {
1568
+ const option = options[vItem.index];
1569
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1570
+ "div",
1571
+ {
1572
+ style: {
1573
+ position: "absolute",
1574
+ top: 0,
1575
+ left: 0,
1576
+ width: "100%",
1577
+ transform: `translateY(${vItem.start}px)`
1578
+ },
1579
+ ref: virtualizer.measureElement,
1580
+ "data-index": vItem.index,
1581
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1582
+ MentionMenuItem,
1583
+ {
1584
+ option,
1585
+ isSelected: selectedIndex === vItem.index,
1586
+ onClick: () => {
1587
+ setHighlightedIndex(vItem.index);
1588
+ selectOptionAndCleanUp(option);
1589
+ },
1590
+ onMouseEnter: () => {
1591
+ setHighlightedIndex(vItem.index);
1592
+ }
1593
+ }
1594
+ )
1595
+ },
1596
+ option.key
1597
+ );
1598
+ })
1599
+ }
1600
+ )
1601
+ }
1602
+ );
1603
+ }
1604
+ function checkForMentionMatch(text, trigger) {
1605
+ const escaped = trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1606
+ const regex = new RegExp(
1607
+ `(^|\\s|\\()([${escaped}]((?:[^${escaped}\\s]){0,75}))$`
1608
+ );
1609
+ const match = regex.exec(text);
1610
+ if (match !== null) {
1611
+ const maybeLeadingWhitespace = match[1];
1612
+ const matchingString = match[3];
1613
+ return {
1614
+ leadOffset: match.index + maybeLeadingWhitespace.length,
1615
+ matchingString,
1616
+ replaceableString: match[2]
1617
+ };
1618
+ }
1619
+ return null;
1620
+ }
1621
+ function MentionPlugin({
1622
+ users,
1623
+ trigger = "@",
1624
+ onMentionInsert
1625
+ }) {
1626
+ const [editor] = (0, import_LexicalComposerContext.useLexicalComposerContext)();
1627
+ const [queryString, setQueryString] = (0, import_react2.useState)(null);
1628
+ const checkForSlashTriggerMatch = (0, import_LexicalTypeaheadMenuPlugin.useBasicTypeaheadTriggerMatch)("/", {
1629
+ minLength: 0
1630
+ });
1631
+ const results = (0, import_react2.useMemo)(() => {
1632
+ if (queryString === null)
1633
+ return users.slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
1634
+ const q = queryString.toLowerCase();
1635
+ return users.filter((u) => u.name.toLowerCase().includes(q)).slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
1636
+ }, [users, queryString]);
1637
+ const options = (0, import_react2.useMemo)(
1638
+ () => results.map((user) => new MentionTypeaheadOption(user)),
1639
+ [results]
1640
+ );
1641
+ const onSelectOption = (0, import_react2.useCallback)(
1642
+ (selectedOption, nodeToReplace, closeMenu) => {
1643
+ editor.update(() => {
1644
+ const mentionNode = $createMentionNode(
1645
+ selectedOption.user.id,
1646
+ selectedOption.user.name
1647
+ );
1648
+ if (nodeToReplace) {
1649
+ nodeToReplace.replace(mentionNode);
1650
+ }
1651
+ const spaceNode = (0, import_lexical3.$createTextNode)(" ");
1652
+ mentionNode.insertAfter(spaceNode);
1653
+ spaceNode.selectEnd();
1654
+ closeMenu();
1655
+ });
1656
+ onMentionInsert?.(selectedOption.user);
1657
+ },
1658
+ [editor, onMentionInsert]
1659
+ );
1660
+ const checkForMentionTrigger = (0, import_react2.useCallback)(
1661
+ (text) => {
1662
+ const slashMatch = checkForSlashTriggerMatch(text, editor);
1663
+ if (slashMatch !== null) {
1664
+ return null;
1665
+ }
1666
+ return checkForMentionMatch(text, trigger);
1667
+ },
1668
+ [checkForSlashTriggerMatch, editor, trigger]
1669
+ );
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1671
+ import_LexicalTypeaheadMenuPlugin.LexicalTypeaheadMenuPlugin,
1672
+ {
1673
+ onQueryChange: setQueryString,
1674
+ onSelectOption,
1675
+ triggerFn: checkForMentionTrigger,
1676
+ options,
1677
+ menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => anchorElementRef.current && options.length ? ReactDOM.createPortal(
1678
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "cat-mention-popover", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1679
+ VirtualizedMentionMenu,
1680
+ {
1681
+ options,
1682
+ selectedIndex,
1683
+ selectOptionAndCleanUp,
1684
+ setHighlightedIndex
1685
+ }
1686
+ ) }),
1687
+ anchorElementRef.current
1688
+ ) : null
1689
+ }
1690
+ );
1691
+ }
1692
+
1693
+ // src/layout/cat-editor/plugins.tsx
1694
+ var import_LexicalComposerContext2 = require("@lexical/react/LexicalComposerContext");
1695
+ var import_selection = require("@lexical/selection");
1696
+ var import_lexical5 = require("lexical");
1697
+ var import_react3 = require("react");
1698
+
1699
+ // src/utils/detect-quotes.ts
1700
+ var BUILTIN_ESCAPE_PATTERNS = {
1701
+ english: [
1702
+ "n't",
1703
+ // don't, can't, won't, shouldn't, …
1704
+ "'s",
1705
+ // it's, he's, she's, …
1706
+ "'re",
1707
+ // they're, we're, you're, …
1708
+ "'ve",
1709
+ // I've, they've, we've, …
1710
+ "'ll",
1711
+ // I'll, you'll, they'll, …
1712
+ "'m",
1713
+ // I'm
1714
+ "'d"
1715
+ // I'd, they'd, …
1716
+ ],
1717
+ default: []
1718
+ };
1719
+ function resolveEscapeSuffixes(opts) {
1720
+ const patternsOpt = opts.escapePatterns ?? "english";
1721
+ let patterns;
1722
+ if (typeof patternsOpt === "string") {
1723
+ patterns = BUILTIN_ESCAPE_PATTERNS;
1724
+ return patterns[patternsOpt] ?? [];
1725
+ }
1726
+ return Object.values(patternsOpt).flat();
1727
+ }
1728
+ function isContractionApostrophe(text, index, suffixes) {
1729
+ for (const suffix of suffixes) {
1730
+ const apostrophePositions = [];
1731
+ for (let i = 0; i < suffix.length; i++) {
1732
+ if (suffix[i] === "'") apostrophePositions.push(i);
1733
+ }
1734
+ for (const ap of apostrophePositions) {
1735
+ const suffixStart = index - ap;
1736
+ if (suffixStart < 0) continue;
1737
+ const suffixEnd = suffixStart + suffix.length;
1738
+ if (suffixEnd > text.length) continue;
1739
+ const slice = text.slice(suffixStart, suffixEnd);
1740
+ if (slice !== suffix) continue;
1741
+ if (suffixStart > 0 && /\w/.test(text[suffixStart - 1])) {
1742
+ return true;
1743
+ }
1744
+ }
1745
+ }
1746
+ return false;
1747
+ }
1748
+ function detectQuotes(text, options = {}) {
1749
+ const {
1750
+ escapeContractions = true,
1751
+ allowNesting = false,
1752
+ detectInnerQuotes = true
1753
+ } = options;
1754
+ const escapeSuffixes = escapeContractions ? resolveEscapeSuffixes(options) : [];
1755
+ const result = /* @__PURE__ */ new Map();
1756
+ let openSingle = null;
1757
+ let openDouble = null;
1758
+ for (let i = 0; i < text.length; i++) {
1759
+ const ch = text[i];
1760
+ if (ch === "\\") {
1761
+ i++;
1762
+ continue;
1763
+ }
1764
+ if (ch === '"') {
1765
+ if (!allowNesting && !detectInnerQuotes && openSingle) continue;
1766
+ if (openDouble) {
1767
+ if (!allowNesting && openSingle && openSingle.start > openDouble.start) {
1768
+ openSingle = null;
1769
+ }
1770
+ const range = {
1771
+ start: openDouble.start,
1772
+ end: i,
1773
+ quoteType: "double",
1774
+ content: text.slice(openDouble.start + 1, i),
1775
+ closed: true
1776
+ };
1777
+ result.set(openDouble.start, range);
1778
+ result.set(i, range);
1779
+ openDouble = null;
1780
+ } else {
1781
+ openDouble = { start: i };
1782
+ }
1783
+ continue;
1784
+ }
1785
+ if (ch === "'") {
1786
+ if (!allowNesting && !detectInnerQuotes && openDouble) continue;
1787
+ if (escapeContractions && escapeSuffixes.length > 0 && isContractionApostrophe(text, i, escapeSuffixes)) {
1788
+ continue;
1789
+ }
1790
+ if (openSingle) {
1791
+ if (!allowNesting && openDouble && openDouble.start > openSingle.start) {
1792
+ openDouble = null;
1793
+ }
1794
+ const range = {
1795
+ start: openSingle.start,
1796
+ end: i,
1797
+ quoteType: "single",
1798
+ content: text.slice(openSingle.start + 1, i),
1799
+ closed: true
1800
+ };
1801
+ result.set(openSingle.start, range);
1802
+ result.set(i, range);
1803
+ openSingle = null;
1804
+ } else {
1805
+ openSingle = { start: i };
1806
+ }
1807
+ continue;
1808
+ }
1809
+ }
1810
+ if (openSingle) {
1811
+ result.set(openSingle.start, {
1812
+ start: openSingle.start,
1813
+ end: null,
1814
+ quoteType: "single",
1815
+ content: text.slice(openSingle.start + 1),
1816
+ closed: false
1817
+ });
1818
+ }
1819
+ if (openDouble) {
1820
+ result.set(openDouble.start, {
1821
+ start: openDouble.start,
1822
+ end: null,
1823
+ quoteType: "double",
1824
+ content: text.slice(openDouble.start + 1),
1825
+ closed: false
1826
+ });
1827
+ }
1828
+ return result;
1829
+ }
1830
+
1831
+ // src/layout/cat-editor/compute-segments.ts
1832
+ var TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>/g;
1833
+ function detectAndPairTags(text, _detectInner = true) {
1834
+ const allTags = [];
1835
+ TAG_RE.lastIndex = 0;
1836
+ let m;
1837
+ while ((m = TAG_RE.exec(text)) !== null) {
1838
+ const isClosing = m[1] === "/";
1839
+ const isSelfClosing = m[3] === "/" || !isClosing && m[0].endsWith("/>");
1840
+ allTags.push({
1841
+ start: m.index,
1842
+ end: m.index + m[0].length,
1843
+ tagName: m[2].toLowerCase(),
1844
+ isClosing,
1845
+ isSelfClosing,
1846
+ originalText: m[0]
1847
+ });
1848
+ }
1849
+ let nextNum = 1;
1850
+ const stack = [];
1851
+ const result = [];
1852
+ for (let i = 0; i < allTags.length; i++) {
1853
+ const tag = allTags[i];
1854
+ if (tag.isSelfClosing) {
1855
+ const num = nextNum++;
1856
+ result.push({
1857
+ ...tag,
1858
+ tagNumber: num,
1859
+ displayText: `<${num}/>`
1860
+ });
1861
+ } else if (!tag.isClosing) {
1862
+ const num = nextNum++;
1863
+ stack.push({ name: tag.tagName, num, idx: i });
1864
+ } else {
1865
+ let matchIdx = -1;
1866
+ for (let j = stack.length - 1; j >= 0; j--) {
1867
+ if (stack[j].name === tag.tagName) {
1868
+ matchIdx = j;
1869
+ break;
1870
+ }
1871
+ }
1872
+ if (matchIdx >= 0) {
1873
+ const openEntry = stack[matchIdx];
1874
+ stack.splice(matchIdx, 1);
1875
+ const openTag = allTags[openEntry.idx];
1876
+ result.push({
1877
+ ...openTag,
1878
+ tagNumber: openEntry.num,
1879
+ displayText: `<${openEntry.num}>`
1880
+ });
1881
+ result.push({
1882
+ ...tag,
1883
+ tagNumber: openEntry.num,
1884
+ displayText: `</${openEntry.num}>`
1885
+ });
1886
+ }
1887
+ }
1888
+ }
1889
+ result.sort((a, b) => a.start - b.start);
1890
+ return result;
1891
+ }
1892
+ var HTML_TAG_CLASSIFY = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>$/;
1893
+ function detectCustomTags(text, patternSource) {
1894
+ let re;
1895
+ try {
1896
+ re = new RegExp(patternSource, "g");
1897
+ } catch {
1898
+ return [];
1899
+ }
1900
+ const matches = [];
1901
+ let m;
1902
+ while ((m = re.exec(text)) !== null) {
1903
+ if (m[0].length === 0) {
1904
+ re.lastIndex++;
1905
+ continue;
1906
+ }
1907
+ const htmlMatch = HTML_TAG_CLASSIFY.exec(m[0]);
1908
+ if (htmlMatch) {
1909
+ matches.push({
1910
+ start: m.index,
1911
+ end: m.index + m[0].length,
1912
+ text: m[0],
1913
+ htmlName: htmlMatch[2].toLowerCase(),
1914
+ isClosing: htmlMatch[1] === "/",
1915
+ isSelfClosing: htmlMatch[3] === "/" || !htmlMatch[1] && m[0].endsWith("/>")
1916
+ });
1917
+ } else {
1918
+ matches.push({
1919
+ start: m.index,
1920
+ end: m.index + m[0].length,
1921
+ text: m[0],
1922
+ htmlName: null,
1923
+ isClosing: false,
1924
+ isSelfClosing: false
1925
+ });
1926
+ }
1927
+ }
1928
+ let nextNum = 1;
1929
+ const stack = [];
1930
+ const result = [];
1931
+ for (let i = 0; i < matches.length; i++) {
1932
+ const raw = matches[i];
1933
+ if (!raw.htmlName) {
1934
+ const num = nextNum++;
1935
+ result.push({
1936
+ start: raw.start,
1937
+ end: raw.end,
1938
+ tagName: raw.text,
1939
+ tagNumber: num,
1940
+ isClosing: false,
1941
+ isSelfClosing: false,
1942
+ originalText: raw.text,
1943
+ displayText: `<${num}>`
1944
+ });
1945
+ continue;
1946
+ }
1947
+ if (raw.isSelfClosing) {
1948
+ const num = nextNum++;
1949
+ result.push({
1950
+ start: raw.start,
1951
+ end: raw.end,
1952
+ tagName: raw.htmlName,
1953
+ tagNumber: num,
1954
+ isClosing: false,
1955
+ isSelfClosing: true,
1956
+ originalText: raw.text,
1957
+ displayText: `<${num}/>`
1958
+ });
1959
+ } else if (!raw.isClosing) {
1960
+ const num = nextNum++;
1961
+ stack.push({ name: raw.htmlName, num, idx: i });
1962
+ } else {
1963
+ let matchIdx = -1;
1964
+ for (let j = stack.length - 1; j >= 0; j--) {
1965
+ if (stack[j].name === raw.htmlName) {
1966
+ matchIdx = j;
1967
+ break;
1968
+ }
1969
+ }
1970
+ if (matchIdx >= 0) {
1971
+ const openEntry = stack[matchIdx];
1972
+ stack.splice(matchIdx, 1);
1973
+ const openRaw = matches[openEntry.idx];
1974
+ result.push({
1975
+ start: openRaw.start,
1976
+ end: openRaw.end,
1977
+ tagName: openRaw.htmlName,
1978
+ tagNumber: openEntry.num,
1979
+ isClosing: false,
1980
+ isSelfClosing: false,
1981
+ originalText: openRaw.text,
1982
+ displayText: `<${openEntry.num}>`
1983
+ });
1984
+ result.push({
1985
+ start: raw.start,
1986
+ end: raw.end,
1987
+ tagName: raw.htmlName,
1988
+ tagNumber: openEntry.num,
1989
+ isClosing: true,
1990
+ isSelfClosing: false,
1991
+ originalText: raw.text,
1992
+ displayText: `</${openEntry.num}>`
1993
+ });
1994
+ }
1995
+ }
1996
+ }
1997
+ result.sort((a, b) => a.start - b.start);
1998
+ return result;
1999
+ }
2000
+ function computeHighlightSegments(text, rules) {
2001
+ const rawRanges = [];
2002
+ for (const rule of rules) {
2003
+ if (rule.type === "spellcheck") {
2004
+ for (const v of rule.validations) {
2005
+ if (v.start < 0 || v.start >= v.end || !v.content) continue;
2006
+ let matchStart = -1;
2007
+ let matchEnd = -1;
2008
+ if (v.end <= text.length && text.slice(v.start, v.end) === v.content) {
2009
+ matchStart = v.start;
2010
+ matchEnd = v.end;
2011
+ } else {
2012
+ const searchRadius = Math.max(64, v.content.length * 4);
2013
+ const searchFrom = Math.max(0, v.start - searchRadius);
2014
+ const searchTo = Math.min(text.length, v.end + searchRadius);
2015
+ const regionLower = text.slice(searchFrom, searchTo).toLowerCase();
2016
+ const contentLower = v.content.toLowerCase();
2017
+ const idx = regionLower.indexOf(contentLower);
2018
+ if (idx !== -1) {
2019
+ matchStart = searchFrom + idx;
2020
+ matchEnd = matchStart + v.content.length;
2021
+ }
2022
+ }
2023
+ if (matchStart >= 0) {
2024
+ rawRanges.push({
2025
+ start: matchStart,
2026
+ end: matchEnd,
2027
+ annotation: {
2028
+ type: "spellcheck",
2029
+ id: `sc-${matchStart}-${matchEnd}`,
2030
+ data: v
2031
+ }
2032
+ });
2033
+ }
2034
+ }
2035
+ } else if (rule.type === "glossary") {
2036
+ const { label, entries } = rule;
2037
+ for (const entry of entries) {
2038
+ if (!entry.term && !entry.pattern) continue;
2039
+ if (entry.pattern) {
2040
+ let re;
2041
+ try {
2042
+ re = new RegExp(entry.pattern, "g");
2043
+ } catch {
2044
+ continue;
2045
+ }
2046
+ let m;
2047
+ while ((m = re.exec(text)) !== null) {
2048
+ rawRanges.push({
2049
+ start: m.index,
2050
+ end: m.index + m[0].length,
2051
+ annotation: {
2052
+ type: "glossary",
2053
+ id: `gl-${label}-${m.index}-${m.index + m[0].length}`,
2054
+ data: {
2055
+ label,
2056
+ term: entry.term || entry.pattern,
2057
+ description: entry.description
2058
+ }
2059
+ }
2060
+ });
2061
+ if (m[0].length === 0) re.lastIndex++;
2062
+ }
2063
+ } else {
2064
+ let idx = 0;
2065
+ while ((idx = text.indexOf(entry.term, idx)) !== -1) {
2066
+ rawRanges.push({
2067
+ start: idx,
2068
+ end: idx + entry.term.length,
2069
+ annotation: {
2070
+ type: "glossary",
2071
+ id: `gl-${label}-${idx}-${idx + entry.term.length}`,
2072
+ data: {
2073
+ label,
2074
+ term: entry.term,
2075
+ description: entry.description
2076
+ }
2077
+ }
2078
+ });
2079
+ idx += entry.term.length;
2080
+ }
2081
+ }
2082
+ }
2083
+ } else if (rule.type === "tag") {
2084
+ const pairs = rule.pattern ? detectCustomTags(text, rule.pattern) : detectAndPairTags(text, rule.detectInner ?? true);
2085
+ for (const p of pairs) {
2086
+ const isHtml = !rule.pattern || p.tagName !== p.originalText;
2087
+ rawRanges.push({
2088
+ start: p.start,
2089
+ end: p.end,
2090
+ annotation: {
2091
+ type: "tag",
2092
+ id: `tag-${p.start}-${p.end}`,
2093
+ data: {
2094
+ tagNumber: p.tagNumber,
2095
+ tagName: p.tagName,
2096
+ isClosing: p.isClosing,
2097
+ isSelfClosing: p.isSelfClosing,
2098
+ originalText: p.originalText,
2099
+ displayText: p.displayText,
2100
+ isHtml
2101
+ }
2102
+ }
2103
+ });
2104
+ }
2105
+ } else if (rule.type === "quote") {
2106
+ const quoteMap = detectQuotes(text, rule.detectOptions);
2107
+ const seen = /* @__PURE__ */ new Set();
2108
+ for (const [, qr] of quoteMap) {
2109
+ if (seen.has(qr.start)) continue;
2110
+ seen.add(qr.start);
2111
+ const mapping = qr.quoteType === "single" ? rule.singleQuote : rule.doubleQuote;
2112
+ const originalChar = qr.quoteType === "single" ? "'" : '"';
2113
+ rawRanges.push({
2114
+ start: qr.start,
2115
+ end: qr.start + 1,
2116
+ annotation: {
2117
+ type: "quote",
2118
+ id: `q-${qr.quoteType}-open-${qr.start}`,
2119
+ data: {
2120
+ quoteType: qr.quoteType,
2121
+ position: "opening",
2122
+ originalChar,
2123
+ replacementChar: mapping.opening
2124
+ }
2125
+ }
2126
+ });
2127
+ if (qr.closed && qr.end !== null) {
2128
+ rawRanges.push({
2129
+ start: qr.end,
2130
+ end: qr.end + 1,
2131
+ annotation: {
2132
+ type: "quote",
2133
+ id: `q-${qr.quoteType}-close-${qr.end}`,
2134
+ data: {
2135
+ quoteType: qr.quoteType,
2136
+ position: "closing",
2137
+ originalChar,
2138
+ replacementChar: mapping.closing
2139
+ }
2140
+ }
2141
+ });
2142
+ }
2143
+ }
2144
+ } else if (rule.type === "special-char") {
2145
+ const allEntries = [...rule.entries];
2146
+ for (const entry of allEntries) {
2147
+ const flags = entry.pattern.flags.includes("g") ? entry.pattern.flags : entry.pattern.flags + "g";
2148
+ const re = new RegExp(entry.pattern.source, flags);
2149
+ let m;
2150
+ while ((m = re.exec(text)) !== null) {
2151
+ const matchStr = m[0];
2152
+ const cp = matchStr.split("").map(
2153
+ (c) => "U+" + (c.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")
2154
+ ).join(" ");
2155
+ rawRanges.push({
2156
+ start: m.index,
2157
+ end: m.index + matchStr.length,
2158
+ annotation: {
2159
+ type: "special-char",
2160
+ id: `sp-${m.index}-${m.index + matchStr.length}`,
2161
+ data: { name: entry.name, char: matchStr, codePoint: cp }
2162
+ }
2163
+ });
2164
+ }
2165
+ }
2166
+ } else if (rule.type === "link") {
2167
+ const defaultPattern = String.raw`https?:\/\/[^\s<>"']+|www\.[^\s<>"']+`;
2168
+ const patternSource = rule.pattern ?? defaultPattern;
2169
+ let re;
2170
+ try {
2171
+ re = new RegExp(patternSource, "gi");
2172
+ } catch {
2173
+ continue;
2174
+ }
2175
+ let m;
2176
+ while ((m = re.exec(text)) !== null) {
2177
+ if (m[0].length === 0) {
2178
+ re.lastIndex++;
2179
+ continue;
2180
+ }
2181
+ let matched = m[0];
2182
+ const trailingPunct = /[.,;:!?)}\]]+$/;
2183
+ const trailingMatch = trailingPunct.exec(matched);
2184
+ if (trailingMatch) {
2185
+ matched = matched.slice(0, -trailingMatch[0].length);
2186
+ }
2187
+ const end = m.index + matched.length;
2188
+ const url = matched.startsWith("www.") ? "https://" + matched : matched;
2189
+ rawRanges.push({
2190
+ start: m.index,
2191
+ end,
2192
+ annotation: {
2193
+ type: "link",
2194
+ id: `link-${m.index}-${end}`,
2195
+ data: {
2196
+ url,
2197
+ displayText: matched
2198
+ }
2199
+ }
2200
+ });
2201
+ }
2202
+ }
2203
+ }
2204
+ if (rawRanges.length === 0) return [];
2205
+ const tagRules = rules.filter((r) => r.type === "tag");
2206
+ const tagsCollapsed = tagRules.some((r) => r.collapsed);
2207
+ const tagRanges = rawRanges.filter((r) => r.annotation.type === "tag");
2208
+ const quoteDetectInTags = rules.filter((r) => r.type === "quote").some((r) => r.detectInTags);
2209
+ let filteredRanges = rawRanges;
2210
+ if (tagRanges.length > 0) {
2211
+ filteredRanges = rawRanges.filter((r) => {
2212
+ if (r.annotation.type === "tag") return true;
2213
+ if (r.annotation.type === "quote" && !quoteDetectInTags) {
2214
+ return !tagRanges.some((t) => r.start >= t.start && r.end <= t.end);
2215
+ }
2216
+ if (tagsCollapsed) {
2217
+ return !tagRanges.some((t) => r.start < t.end && r.end > t.start);
2218
+ }
2219
+ return true;
2220
+ });
2221
+ }
2222
+ const points = /* @__PURE__ */ new Set();
2223
+ for (const r of filteredRanges) {
2224
+ points.add(r.start);
2225
+ points.add(r.end);
2226
+ }
2227
+ const sortedPoints = [...points].sort((a, b) => a - b);
2228
+ const segments = [];
2229
+ for (let i = 0; i < sortedPoints.length - 1; i++) {
2230
+ const segStart = sortedPoints[i];
2231
+ const segEnd = sortedPoints[i + 1];
2232
+ const annotations = filteredRanges.filter((r) => r.start <= segStart && r.end >= segEnd).map((r) => r.annotation);
2233
+ if (annotations.length > 0) {
2234
+ segments.push({ start: segStart, end: segEnd, annotations });
2235
+ }
2236
+ }
2237
+ return segments;
2238
+ }
2239
+
2240
+ // src/layout/cat-editor/selection-helpers.ts
2241
+ var import_lexical4 = require("lexical");
2242
+ function $isCEFalseToken(node) {
2243
+ const types = node.__highlightTypes.split(",");
2244
+ if (types.includes("tag-collapsed")) return true;
2245
+ if (types.includes("quote") && node.__displayText) return true;
2246
+ return false;
2247
+ }
2248
+ function $pointToGlobalOffset(nodeKey, offset) {
2249
+ const root = (0, import_lexical4.$getRoot)();
2250
+ const paragraphs = root.getChildren();
2251
+ let global = 0;
2252
+ for (let pi = 0; pi < paragraphs.length; pi++) {
2253
+ if (pi > 0) global += 1;
2254
+ const p = paragraphs[pi];
2255
+ if (p.getKey() === nodeKey) {
2256
+ if ("getChildren" in p) {
2257
+ const children = p.getChildren();
2258
+ let childChars = 0;
2259
+ for (let ci = 0; ci < Math.min(offset, children.length); ci++) {
2260
+ const child = children[ci];
2261
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
2262
+ continue;
2263
+ childChars += child.getTextContent().length;
2264
+ }
2265
+ return global + childChars;
2266
+ }
2267
+ return global;
2268
+ }
2269
+ if (!("getChildren" in p)) continue;
2270
+ for (const child of p.getChildren()) {
2271
+ const isNlMarker = $isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX);
2272
+ if (child.getKey() === nodeKey) {
2273
+ if (isNlMarker) return global;
2274
+ if ($isHighlightNode(child) && child.getMode() === "token") {
2275
+ return global + (offset > 0 ? child.getTextContent().length : 0);
2276
+ }
2277
+ if ($isMentionNode(child)) {
2278
+ return global + (offset > 0 ? child.getTextContent().length : 0);
2279
+ }
2280
+ return global + offset;
2281
+ }
2282
+ if (isNlMarker) continue;
2283
+ global += child.getTextContent().length;
2284
+ }
2285
+ }
2286
+ return global;
2287
+ }
2288
+ function $globalOffsetToPoint(target) {
2289
+ const root = (0, import_lexical4.$getRoot)();
2290
+ const paragraphs = root.getChildren();
2291
+ let remaining = target;
2292
+ for (let pi = 0; pi < paragraphs.length; pi++) {
2293
+ if (pi > 0) {
2294
+ if (remaining <= 0) {
2295
+ const p2 = paragraphs[pi];
2296
+ if ("getChildren" in p2) {
2297
+ for (const child of p2.getChildren()) {
2298
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
2299
+ continue;
2300
+ return { key: child.getKey(), offset: 0, type: "text" };
2301
+ }
2302
+ }
2303
+ return { key: paragraphs[pi].getKey(), offset: 0, type: "element" };
2304
+ }
2305
+ remaining -= 1;
2306
+ }
2307
+ const p = paragraphs[pi];
2308
+ if (!("getChildren" in p)) continue;
2309
+ const allChildren = p.getChildren();
2310
+ for (let ci = 0; ci < allChildren.length; ci++) {
2311
+ const child = allChildren[ci];
2312
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
2313
+ continue;
2314
+ const len = child.getTextContent().length;
2315
+ if ($isHighlightNode(child) && $isCEFalseToken(child)) {
2316
+ if (remaining <= 0) {
2317
+ return { key: p.getKey(), offset: ci, type: "element" };
2318
+ }
2319
+ remaining -= len;
2320
+ continue;
2321
+ }
2322
+ if ($isMentionNode(child)) {
2323
+ if (remaining <= 0) {
2324
+ return { key: p.getKey(), offset: ci, type: "element" };
2325
+ }
2326
+ remaining -= len;
2327
+ continue;
2328
+ }
2329
+ if ($isHighlightNode(child) && child.getMode() === "token") {
2330
+ if (remaining > len) {
2331
+ remaining -= len;
2332
+ continue;
2333
+ }
2334
+ return {
2335
+ key: child.getKey(),
2336
+ offset: remaining <= 0 ? 0 : remaining >= len ? len : remaining <= len / 2 ? 0 : len,
2337
+ type: "text"
2338
+ };
2339
+ }
2340
+ if (remaining <= len) {
2341
+ return {
2342
+ key: child.getKey(),
2343
+ offset: Math.max(0, remaining),
2344
+ type: "text"
2345
+ };
2346
+ }
2347
+ remaining -= len;
2348
+ }
2349
+ if (remaining <= 0) {
2350
+ let afterIdx = allChildren.length;
2351
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
2352
+ const c = allChildren[ci];
2353
+ if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
2354
+ afterIdx = ci;
2355
+ } else {
2356
+ break;
2357
+ }
2358
+ }
2359
+ return { key: p.getKey(), offset: afterIdx, type: "element" };
2360
+ }
2361
+ }
2362
+ for (let pi = paragraphs.length - 1; pi >= 0; pi--) {
2363
+ const p = paragraphs[pi];
2364
+ if ("getChildren" in p) {
2365
+ const allChildren = p.getChildren();
2366
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
2367
+ const child = allChildren[ci];
2368
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
2369
+ continue;
2370
+ if ($isHighlightNode(child) && $isCEFalseToken(child)) continue;
2371
+ if ($isMentionNode(child)) continue;
2372
+ return {
2373
+ key: child.getKey(),
2374
+ offset: child.getTextContent().length,
2375
+ type: "text"
2376
+ };
2377
+ }
2378
+ let afterIdx = allChildren.length;
2379
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
2380
+ const c = allChildren[ci];
2381
+ if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
2382
+ afterIdx = ci;
2383
+ } else {
2384
+ break;
2385
+ }
2386
+ }
2387
+ return { key: p.getKey(), offset: afterIdx, type: "element" };
2388
+ }
2389
+ }
2390
+ return null;
2391
+ }
2392
+
2393
+ // src/layout/cat-editor/plugins.tsx
2394
+ function HighlightsPlugin({
2395
+ rules,
2396
+ annotationMapRef,
2397
+ codepointDisplayMap
2398
+ }) {
2399
+ const [editor] = (0, import_LexicalComposerContext2.useLexicalComposerContext)();
2400
+ const rafRef = (0, import_react3.useRef)(null);
2401
+ const applyHighlights = (0, import_react3.useCallback)(() => {
2402
+ setCodepointOverrides(codepointDisplayMap);
2403
+ const editorElement = editor.getRootElement();
2404
+ const editorHasFocus = editorElement != null && editorElement.contains(editorElement.ownerDocument.activeElement);
2405
+ editor.update(
2406
+ () => {
2407
+ (0, import_lexical5.$addUpdateTag)("cat-highlights");
2408
+ const root = (0, import_lexical5.$getRoot)();
2409
+ const paragraphs = root.getChildren();
2410
+ const lines = [];
2411
+ const savedMentions = [];
2412
+ let collectOffset = 0;
2413
+ for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) {
2414
+ const p = paragraphs[pIdx];
2415
+ let lineText = "";
2416
+ if ("getChildren" in p) {
2417
+ for (const child of p.getChildren()) {
2418
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
2419
+ continue;
2420
+ }
2421
+ if ($isMentionNode(child)) {
2422
+ const text = child.getTextContent();
2423
+ savedMentions.push({
2424
+ start: collectOffset + lineText.length,
2425
+ end: collectOffset + lineText.length + text.length,
2426
+ mentionId: child.__mentionId,
2427
+ mentionName: child.__mentionName,
2428
+ text
2429
+ });
2430
+ }
2431
+ lineText += child.getTextContent();
2432
+ }
2433
+ } else {
2434
+ lineText = p.getTextContent();
2435
+ }
2436
+ lines.push(lineText);
2437
+ collectOffset += lineText.length + 1;
2438
+ }
2439
+ const fullText = lines.join("\n");
2440
+ const mentionRule = rules.find(
2441
+ (r) => r.type === "mention"
2442
+ );
2443
+ if (mentionRule) {
2444
+ const pattern = getMentionPattern();
2445
+ let match;
2446
+ while ((match = pattern.exec(fullText)) !== null) {
2447
+ const matchStart = match.index;
2448
+ const matchEnd = matchStart + match[0].length;
2449
+ const matchId = match[1];
2450
+ const alreadySaved = savedMentions.some(
2451
+ (m) => m.start === matchStart && m.end === matchEnd
2452
+ );
2453
+ if (alreadySaved) continue;
2454
+ const user = mentionRule.users.find((u) => u.id === matchId);
2455
+ if (user) {
2456
+ savedMentions.push({
2457
+ start: matchStart,
2458
+ end: matchEnd,
2459
+ mentionId: user.id,
2460
+ mentionName: user.name,
2461
+ text: match[0]
2462
+ });
2463
+ }
2464
+ }
2465
+ }
2466
+ let segmentText = fullText;
2467
+ for (let mi = savedMentions.length - 1; mi >= 0; mi--) {
2468
+ const m = savedMentions[mi];
2469
+ segmentText = segmentText.slice(0, m.start) + "".repeat(m.end - m.start) + segmentText.slice(m.end);
2470
+ }
2471
+ const segments = computeHighlightSegments(segmentText, rules);
2472
+ const newMap = /* @__PURE__ */ new Map();
2473
+ for (const seg of segments) {
2474
+ for (const ann of seg.annotations) {
2475
+ newMap.set(ann.id, ann);
2476
+ }
2477
+ }
2478
+ annotationMapRef.current = newMap;
2479
+ const prevSelection = (0, import_lexical5.$getSelection)();
2480
+ let savedAnchor = null;
2481
+ let savedFocus = null;
2482
+ if ((0, import_lexical5.$isRangeSelection)(prevSelection)) {
2483
+ savedAnchor = $pointToGlobalOffset(
2484
+ prevSelection.anchor.key,
2485
+ prevSelection.anchor.offset
2486
+ );
2487
+ savedFocus = $pointToGlobalOffset(
2488
+ prevSelection.focus.key,
2489
+ prevSelection.focus.offset
2490
+ );
2491
+ }
2492
+ root.clear();
2493
+ if (fullText.length === 0) {
2494
+ const p = (0, import_lexical5.$createParagraphNode)();
2495
+ p.append((0, import_lexical5.$createTextNode)(""));
2496
+ root.append(p);
2497
+ return;
2498
+ }
2499
+ const emittedMentionStarts = /* @__PURE__ */ new Set();
2500
+ const appendWithMentions = (paragraph, rangeStart, rangeEnd, makeNode) => {
2501
+ const overlapping = savedMentions.filter(
2502
+ (m) => m.start < rangeEnd && m.end > rangeStart && !emittedMentionStarts.has(m.start)
2503
+ );
2504
+ if (overlapping.length === 0) {
2505
+ const text = fullText.slice(rangeStart, rangeEnd);
2506
+ if (text.length > 0) {
2507
+ paragraph.append(makeNode(text));
2508
+ }
2509
+ return;
2510
+ }
2511
+ overlapping.sort((a, b) => a.start - b.start);
2512
+ let cursor = rangeStart;
2513
+ for (const m of overlapping) {
2514
+ const mStart = Math.max(m.start, rangeStart);
2515
+ const mEnd = Math.min(m.end, rangeEnd);
2516
+ if (mStart > cursor) {
2517
+ const beforeText = fullText.slice(cursor, mStart);
2518
+ if (beforeText.length > 0) {
2519
+ paragraph.append(makeNode(beforeText));
2520
+ }
2521
+ }
2522
+ paragraph.append(
2523
+ $createMentionNode(m.mentionId, m.mentionName, m.text)
2524
+ );
2525
+ emittedMentionStarts.add(m.start);
2526
+ cursor = mEnd;
2527
+ }
2528
+ if (cursor < rangeEnd) {
2529
+ const afterText = fullText.slice(cursor, rangeEnd);
2530
+ if (afterText.length > 0) {
2531
+ paragraph.append(makeNode(afterText));
2532
+ }
2533
+ }
2534
+ };
2535
+ const textLines = fullText.split("\n");
2536
+ let globalOffset = 0;
2537
+ for (const line of textLines) {
2538
+ const paragraph = (0, import_lexical5.$createParagraphNode)();
2539
+ const lineStart = globalOffset;
2540
+ const lineEnd = globalOffset + line.length;
2541
+ const lineSegments = segments.filter(
2542
+ (s) => s.start < lineEnd && s.end > lineStart
2543
+ );
2544
+ let pos = lineStart;
2545
+ for (const seg of lineSegments) {
2546
+ const sStart = Math.max(seg.start, lineStart);
2547
+ const sEnd = Math.min(seg.end, lineEnd);
2548
+ if (sStart > pos) {
2549
+ appendWithMentions(
2550
+ paragraph,
2551
+ pos,
2552
+ sStart,
2553
+ (text) => (0, import_lexical5.$createTextNode)(text)
2554
+ );
2555
+ }
2556
+ const tagAnn = seg.annotations.find((a) => a.type === "tag");
2557
+ const tagRule = rules.find((r) => r.type === "tag");
2558
+ const tagsCollapsed = !!tagRule?.collapsed;
2559
+ const collapseScope = tagRule?.collapseScope ?? "all";
2560
+ const thisTagCollapsed = tagsCollapsed && !!tagAnn && (collapseScope === "all" || tagAnn.data.isHtml);
2561
+ const typesArr = [
2562
+ ...new Set(
2563
+ seg.annotations.map((a) => {
2564
+ if (a.type === "glossary") return `glossary-${a.data.label}`;
2565
+ if (a.type === "spellcheck")
2566
+ return `spellcheck-${a.data.categoryId}`;
2567
+ return a.type;
2568
+ })
2569
+ )
2570
+ ];
2571
+ if (thisTagCollapsed) {
2572
+ typesArr.push("tag-collapsed");
2573
+ }
2574
+ const types = typesArr.join(",");
2575
+ const ids = seg.annotations.map((a) => a.id).join(",");
2576
+ const tagDisplayText = tagAnn?.type === "tag" && thisTagCollapsed ? tagAnn.data.displayText : void 0;
2577
+ const isTagToken = thisTagCollapsed;
2578
+ const quoteAnn = seg.annotations.find((a) => a.type === "quote");
2579
+ const quoteDisplayText = quoteAnn?.type === "quote" ? quoteAnn.data.replacementChar : void 0;
2580
+ const isQuoteToken = !!quoteAnn;
2581
+ const containingMention = savedMentions.find(
2582
+ (m) => m.start <= sStart && m.end >= sEnd
2583
+ );
2584
+ if (containingMention) {
2585
+ if (!emittedMentionStarts.has(containingMention.start)) {
2586
+ paragraph.append(
2587
+ $createMentionNode(
2588
+ containingMention.mentionId,
2589
+ containingMention.mentionName,
2590
+ containingMention.text
2591
+ )
2592
+ );
2593
+ emittedMentionStarts.add(containingMention.start);
2594
+ }
2595
+ } else {
2596
+ appendWithMentions(
2597
+ paragraph,
2598
+ sStart,
2599
+ sEnd,
2600
+ (text) => $createHighlightNode(
2601
+ text,
2602
+ types,
2603
+ ids,
2604
+ tagDisplayText ?? quoteDisplayText,
2605
+ isTagToken || isQuoteToken
2606
+ )
2607
+ );
2608
+ }
2609
+ pos = sEnd;
2610
+ }
2611
+ if (pos < lineEnd) {
2612
+ appendWithMentions(
2613
+ paragraph,
2614
+ pos,
2615
+ lineEnd,
2616
+ (text) => (0, import_lexical5.$createTextNode)(text)
2617
+ );
2618
+ }
2619
+ const nlPos = lineEnd;
2620
+ if (nlPos < fullText.length && fullText[nlPos] === "\n") {
2621
+ const nlSegments = segments.filter(
2622
+ (s) => s.start <= nlPos && s.end > nlPos
2623
+ );
2624
+ if (nlSegments.length > 0) {
2625
+ const nlAnns = nlSegments.flatMap((s) => s.annotations);
2626
+ const types = [
2627
+ ...new Set(
2628
+ nlAnns.map((a) => {
2629
+ if (a.type === "glossary") return `glossary-${a.data.label}`;
2630
+ if (a.type === "spellcheck")
2631
+ return `spellcheck-${a.data.categoryId}`;
2632
+ return a.type;
2633
+ })
2634
+ )
2635
+ ].join(",");
2636
+ const ids = nlAnns.map((a) => a.id).join(",");
2637
+ const symbol = CODEPOINT_DISPLAY_MAP[10];
2638
+ if (paragraph.getChildrenSize() === 0) {
2639
+ paragraph.append((0, import_lexical5.$createTextNode)(""));
2640
+ }
2641
+ paragraph.append(
2642
+ $createHighlightNode(symbol, types, NL_MARKER_PREFIX + ids)
2643
+ );
2644
+ }
2645
+ }
2646
+ if (paragraph.getChildrenSize() === 0) {
2647
+ paragraph.append((0, import_lexical5.$createTextNode)(""));
2648
+ }
2649
+ root.append(paragraph);
2650
+ globalOffset = lineEnd + 1;
2651
+ }
2652
+ if (editorHasFocus && savedAnchor !== null && savedFocus !== null) {
2653
+ const anchorPt = $globalOffsetToPoint(savedAnchor);
2654
+ const focusPt = $globalOffsetToPoint(savedFocus);
2655
+ if (anchorPt && focusPt) {
2656
+ const sel = (0, import_lexical5.$createRangeSelection)();
2657
+ sel.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
2658
+ sel.focus.set(focusPt.key, focusPt.offset, focusPt.type);
2659
+ (0, import_lexical5.$setSelection)(sel);
2660
+ }
2661
+ } else {
2662
+ (0, import_lexical5.$setSelection)(null);
2663
+ }
2664
+ },
2665
+ { tag: "historic" }
2666
+ );
2667
+ }, [editor, rules, annotationMapRef]);
2668
+ (0, import_react3.useEffect)(() => {
2669
+ applyHighlights();
2670
+ }, [applyHighlights]);
2671
+ (0, import_react3.useEffect)(() => {
2672
+ const unregister = editor.registerUpdateListener(
2673
+ ({ tags, dirtyElements, dirtyLeaves }) => {
2674
+ if (tags.has("cat-highlights")) return;
2675
+ if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
2676
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
2677
+ rafRef.current = requestAnimationFrame(() => {
2678
+ applyHighlights();
2679
+ });
2680
+ }
2681
+ );
2682
+ return () => {
2683
+ unregister();
2684
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
2685
+ };
2686
+ }, [editor, applyHighlights]);
2687
+ (0, import_react3.useEffect)(() => {
2688
+ return editor.registerCommand(
2689
+ import_lexical5.PASTE_COMMAND,
2690
+ (event) => {
2691
+ const clipboardData = event.clipboardData;
2692
+ if (!clipboardData) return false;
2693
+ const text = clipboardData.getData("text/plain");
2694
+ if (text && text !== text.replace(/\n+$/, "")) {
2695
+ event.preventDefault();
2696
+ const trimmed = text.replace(/\n+$/, "");
2697
+ editor.update(() => {
2698
+ const selection = (0, import_lexical5.$getSelection)();
2699
+ if ((0, import_lexical5.$isRangeSelection)(selection)) {
2700
+ selection.insertRawText(trimmed);
2701
+ }
2702
+ });
2703
+ return true;
2704
+ }
2705
+ return false;
2706
+ },
2707
+ import_lexical5.COMMAND_PRIORITY_HIGH
2708
+ );
2709
+ }, [editor]);
2710
+ return null;
2711
+ }
2712
+ function EditorRefPlugin({
2713
+ editorRef,
2714
+ savedSelectionRef
2715
+ }) {
2716
+ const [editor] = (0, import_LexicalComposerContext2.useLexicalComposerContext)();
2717
+ (0, import_react3.useEffect)(() => {
2718
+ editorRef.current = editor;
2719
+ }, [editor, editorRef]);
2720
+ (0, import_react3.useEffect)(() => {
2721
+ return editor.registerUpdateListener(({ editorState }) => {
2722
+ editorState.read(() => {
2723
+ const sel = (0, import_lexical5.$getSelection)();
2724
+ if ((0, import_lexical5.$isRangeSelection)(sel)) {
2725
+ savedSelectionRef.current = {
2726
+ anchor: $pointToGlobalOffset(sel.anchor.key, sel.anchor.offset),
2727
+ focus: $pointToGlobalOffset(sel.focus.key, sel.focus.offset)
2728
+ };
2729
+ }
2730
+ });
2731
+ });
2732
+ }, [editor, savedSelectionRef]);
2733
+ return null;
2734
+ }
2735
+ function $isNonEditableNode(node) {
2736
+ if (!$isHighlightNode(node)) return false;
2737
+ if (node.__ruleIds.startsWith(NL_MARKER_PREFIX)) return true;
2738
+ const types = node.__highlightTypes.split(",");
2739
+ if (types.includes("tag-collapsed")) return true;
2740
+ if (types.includes("quote") && node.__displayText) return true;
2741
+ return false;
2742
+ }
2743
+ function $clampPointAwayFromNonEditable(point) {
2744
+ if (point.type === "element") return null;
2745
+ const node = point.getNode();
2746
+ if (!$isNonEditableNode(node)) return null;
2747
+ const parent = node.getParent();
2748
+ if (parent && "getChildren" in parent) {
2749
+ const siblings = parent.getChildren();
2750
+ const idx = siblings.findIndex((s) => s.getKey() === node.getKey());
2751
+ if (idx >= 0) {
2752
+ const elemOffset = point.offset > 0 ? idx + 1 : idx;
2753
+ return {
2754
+ key: parent.getKey(),
2755
+ offset: elemOffset,
2756
+ type: "element"
2757
+ };
2758
+ }
2759
+ }
2760
+ return null;
2761
+ }
2762
+ function $findNextEditable(startNode) {
2763
+ if ($isHighlightNode(startNode) && startNode.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
2764
+ const paragraph2 = startNode.getParent();
2765
+ const nextParagraph = paragraph2?.getNextSibling();
2766
+ if (nextParagraph && "getChildren" in nextParagraph) {
2767
+ const first = nextParagraph.getFirstChild();
2768
+ if (first && !$isNonEditableNode(first)) {
2769
+ return { key: first.getKey(), offset: 0, type: "text" };
2770
+ }
2771
+ if (first) {
2772
+ return {
2773
+ key: nextParagraph.getKey(),
2774
+ offset: 0,
2775
+ type: "element"
2776
+ };
2777
+ }
2778
+ }
2779
+ return null;
2780
+ }
2781
+ const next = startNode.getNextSibling();
2782
+ if (next && !$isNonEditableNode(next)) {
2783
+ return { key: next.getKey(), offset: 0, type: "text" };
2784
+ }
2785
+ const paragraph = startNode.getParent();
2786
+ if (paragraph && "getChildren" in paragraph) {
2787
+ const children = paragraph.getChildren();
2788
+ const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
2789
+ if (idx >= 0) {
2790
+ return { key: paragraph.getKey(), offset: idx + 1, type: "element" };
2791
+ }
2792
+ }
2793
+ return null;
2794
+ }
2795
+ function $findPrevEditable(startNode) {
2796
+ const prev = startNode.getPreviousSibling();
2797
+ if (prev && !$isNonEditableNode(prev)) {
2798
+ return {
2799
+ key: prev.getKey(),
2800
+ offset: prev.getTextContent().length,
2801
+ type: "text"
2802
+ };
2803
+ }
2804
+ const paragraph = startNode.getParent();
2805
+ if (paragraph && "getChildren" in paragraph) {
2806
+ const children = paragraph.getChildren();
2807
+ const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
2808
+ if (idx >= 0) {
2809
+ return { key: paragraph.getKey(), offset: idx, type: "element" };
2810
+ }
2811
+ }
2812
+ return null;
2813
+ }
2814
+ function NLMarkerNavigationPlugin() {
2815
+ const [editor] = (0, import_LexicalComposerContext2.useLexicalComposerContext)();
2816
+ (0, import_react3.useEffect)(() => {
2817
+ const unregRight = editor.registerCommand(
2818
+ import_lexical5.KEY_ARROW_RIGHT_COMMAND,
2819
+ (event) => {
2820
+ const selection = (0, import_lexical5.$getSelection)();
2821
+ if (!(0, import_lexical5.$isRangeSelection)(selection) || !selection.isCollapsed())
2822
+ return false;
2823
+ const isRTL = (0, import_selection.$isParentElementRTL)(selection);
2824
+ const { anchor } = selection;
2825
+ const node = anchor.getNode();
2826
+ let adjacentNode = null;
2827
+ if (isRTL) {
2828
+ if (anchor.type === "text") {
2829
+ if (anchor.offset > 0) return false;
2830
+ adjacentNode = node.getPreviousSibling();
2831
+ } else {
2832
+ const children = node.getChildren();
2833
+ adjacentNode = children[anchor.offset - 1] ?? null;
2834
+ }
2835
+ } else {
2836
+ if (anchor.type === "text") {
2837
+ if (anchor.offset < node.getTextContent().length) return false;
2838
+ adjacentNode = node.getNextSibling();
2839
+ } else {
2840
+ const children = node.getChildren();
2841
+ adjacentNode = children[anchor.offset] ?? null;
2842
+ }
2843
+ }
2844
+ if (adjacentNode && $isNonEditableNode(adjacentNode)) {
2845
+ const target = isRTL ? $findPrevEditable(adjacentNode) : $findNextEditable(adjacentNode);
2846
+ if (target) {
2847
+ selection.anchor.set(target.key, target.offset, target.type);
2848
+ selection.focus.set(target.key, target.offset, target.type);
2849
+ event.preventDefault();
2850
+ return true;
2851
+ }
2852
+ return false;
2853
+ }
2854
+ if ($isNonEditableNode(node)) {
2855
+ const target = isRTL ? $findPrevEditable(node) : $findNextEditable(node);
2856
+ if (target) {
2857
+ selection.anchor.set(target.key, target.offset, target.type);
2858
+ selection.focus.set(target.key, target.offset, target.type);
2859
+ event.preventDefault();
2860
+ return true;
2861
+ }
2862
+ return false;
2863
+ }
2864
+ return false;
2865
+ },
2866
+ import_lexical5.COMMAND_PRIORITY_HIGH
2867
+ );
2868
+ const unregLeft = editor.registerCommand(
2869
+ import_lexical5.KEY_ARROW_LEFT_COMMAND,
2870
+ (event) => {
2871
+ const selection = (0, import_lexical5.$getSelection)();
2872
+ if (!(0, import_lexical5.$isRangeSelection)(selection) || !selection.isCollapsed())
2873
+ return false;
2874
+ const isRTL = (0, import_selection.$isParentElementRTL)(selection);
2875
+ const { anchor } = selection;
2876
+ const node = anchor.getNode();
2877
+ let adjacentNode = null;
2878
+ if (isRTL) {
2879
+ if (anchor.type === "text") {
2880
+ if (anchor.offset < node.getTextContent().length) return false;
2881
+ adjacentNode = node.getNextSibling();
2882
+ } else {
2883
+ const children = node.getChildren();
2884
+ adjacentNode = children[anchor.offset] ?? null;
2885
+ }
2886
+ } else {
2887
+ if (anchor.type === "text") {
2888
+ if (anchor.offset > 0) return false;
2889
+ adjacentNode = node.getPreviousSibling();
2890
+ } else {
2891
+ const children = node.getChildren();
2892
+ adjacentNode = children[anchor.offset - 1] ?? null;
2893
+ }
2894
+ }
2895
+ if (adjacentNode && $isNonEditableNode(adjacentNode)) {
2896
+ const target = isRTL ? $findNextEditable(adjacentNode) : $findPrevEditable(adjacentNode);
2897
+ if (target) {
2898
+ selection.anchor.set(target.key, target.offset, target.type);
2899
+ selection.focus.set(target.key, target.offset, target.type);
2900
+ event.preventDefault();
2901
+ return true;
2902
+ }
2903
+ return false;
2904
+ }
2905
+ if ($isNonEditableNode(node)) {
2906
+ const target = isRTL ? $findNextEditable(node) : $findPrevEditable(node);
2907
+ if (target) {
2908
+ selection.anchor.set(target.key, target.offset, target.type);
2909
+ selection.focus.set(target.key, target.offset, target.type);
2910
+ event.preventDefault();
2911
+ return true;
2912
+ }
2913
+ return false;
2914
+ }
2915
+ return false;
2916
+ },
2917
+ import_lexical5.COMMAND_PRIORITY_HIGH
2918
+ );
2919
+ const unregSel = editor.registerCommand(
2920
+ import_lexical5.SELECTION_CHANGE_COMMAND,
2921
+ () => {
2922
+ const selection = (0, import_lexical5.$getSelection)();
2923
+ if (!(0, import_lexical5.$isRangeSelection)(selection)) return false;
2924
+ const anchorFix = $clampPointAwayFromNonEditable(selection.anchor);
2925
+ const focusFix = $clampPointAwayFromNonEditable(selection.focus);
2926
+ if (anchorFix) {
2927
+ selection.anchor.set(anchorFix.key, anchorFix.offset, anchorFix.type);
2928
+ }
2929
+ if (focusFix) {
2930
+ selection.focus.set(focusFix.key, focusFix.offset, focusFix.type);
2931
+ }
2932
+ return false;
2933
+ },
2934
+ import_lexical5.COMMAND_PRIORITY_HIGH
2935
+ );
2936
+ return () => {
2937
+ unregRight();
2938
+ unregLeft();
2939
+ unregSel();
2940
+ };
2941
+ }, [editor]);
2942
+ return null;
2943
+ }
2944
+
2945
+ // src/layout/cat-editor/popover.tsx
2946
+ var React2 = __toESM(require("react"), 1);
2947
+ var import_react4 = require("react");
2948
+ var import_core2 = require("@popperjs/core");
2949
+ var import_jsx_runtime3 = require("react/jsx-runtime");
2950
+ function SpellCheckPopoverContent({
2951
+ data,
2952
+ onSuggestionClick
2953
+ }) {
2954
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-2.5 p-3 max-w-sm", children: [
2955
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cat-badge cat-badge-spell", children: data.shortMessage || "Spelling" }),
2957
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[11px] text-muted-foreground", children: data.categoryId })
2958
+ ] }),
2959
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm leading-relaxed text-foreground", children: data.message }),
2960
+ data.content && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
2961
+ "Found:",
2962
+ " ",
2963
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-destructive-foreground", children: data.content })
2964
+ ] }),
2965
+ data.suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-1.5", children: [
2966
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: "Suggestions:" }),
2967
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-wrap gap-1", children: data.suggestions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2968
+ "button",
2969
+ {
2970
+ type: "button",
2971
+ className: "cat-suggestion-btn",
2972
+ onClick: () => onSuggestionClick(s.value),
2973
+ children: s.value
2974
+ },
2975
+ i
2976
+ )) })
2977
+ ] }),
2978
+ data.dictionaries && data.dictionaries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-[11px] text-muted-foreground", children: [
2979
+ "Dictionaries: ",
2980
+ data.dictionaries.join(", ")
2981
+ ] })
2982
+ ] });
2983
+ }
2984
+ function KeywordsPopoverContent({
2985
+ data
2986
+ }) {
2987
+ const displayLabel = data.label.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2988
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "p-3 max-w-xs space-y-2", children: [
2989
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2990
+ "span",
2991
+ {
2992
+ className: `cat-badge cat-badge-glossary cat-badge-glossary-${data.label}`,
2993
+ children: displayLabel
2994
+ }
2995
+ ),
2996
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-sm leading-relaxed text-foreground", children: [
2997
+ "Term:",
2998
+ " ",
2999
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { className: "font-semibold text-foreground", children: data.term })
3000
+ ] }),
3001
+ data.description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-muted-foreground leading-relaxed", children: data.description })
3002
+ ] });
3003
+ }
3004
+ function SpecialCharPopoverContent({
3005
+ data
3006
+ }) {
3007
+ const cp = data.char.codePointAt(0) ?? 0;
3008
+ const effectiveMap = getEffectiveCodepointMap();
3009
+ const displaySymbol = effectiveMap[cp] ?? (data.char.trim() === "" ? "\xB7" : data.char);
3010
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "p-3 max-w-xs space-y-3", children: [
3011
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cat-badge cat-badge-special-char", children: "Special Char" }),
3012
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.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 }) }),
3013
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm leading-relaxed text-foreground text-center", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { className: "font-semibold", children: data.name }) }),
3014
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center justify-center gap-3 text-xs text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.codePoint }) })
3015
+ ] });
3016
+ }
3017
+ function TagPopoverContent({
3018
+ data
3019
+ }) {
3020
+ const isPlaceholder = !data.isClosing && !data.isSelfClosing && data.tagName === data.originalText;
3021
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "p-3 max-w-xs space-y-2", children: [
3022
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "cat-badge cat-badge-tag", children: [
3023
+ isPlaceholder ? "Placeholder" : "Tag",
3024
+ " #",
3025
+ data.tagNumber
3026
+ ] }),
3027
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-sm leading-relaxed text-foreground", children: [
3028
+ isPlaceholder ? "Placeholder" : data.isClosing ? "Closing tag" : data.isSelfClosing ? "Self-closing tag" : "Opening tag",
3029
+ ":",
3030
+ " ",
3031
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { className: "font-semibold text-foreground", children: data.originalText })
3032
+ ] }),
3033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
3034
+ "Collapsed:",
3035
+ " ",
3036
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.displayText })
3037
+ ] }),
3038
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-muted-foreground break-all", children: [
3039
+ "Original:",
3040
+ " ",
3041
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.originalText })
3042
+ ] })
3043
+ ] });
3044
+ }
3045
+ function LinkPopoverContent({
3046
+ data,
3047
+ onOpen
3048
+ }) {
3049
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "p-3 max-w-xs space-y-2", children: [
3050
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "cat-badge cat-badge-link", children: "Link" }),
3051
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm leading-relaxed text-foreground break-all", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-xs", children: data.url }) }),
3052
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "cat-suggestion-btn", onClick: onOpen, children: "Open link \u2197" })
3053
+ ] });
3054
+ }
3055
+ function QuotePopoverContent({
3056
+ data
3057
+ }) {
3058
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "p-3 max-w-xs space-y-2", children: [
3059
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3060
+ "span",
3061
+ {
3062
+ className: `cat-badge cat-badge-quote cat-badge-quote-${data.quoteType}`,
3063
+ children: data.quoteType === "single" ? "Single Quote" : "Double Quote"
3064
+ }
3065
+ ),
3066
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-sm leading-relaxed text-foreground", children: [
3067
+ data.position === "opening" ? "Opening" : "Closing",
3068
+ " quote"
3069
+ ] }),
3070
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
3071
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.originalChar }),
3072
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u2192" }),
3073
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.replacementChar })
3074
+ ] })
3075
+ ] });
3076
+ }
3077
+ function HighlightPopover({
3078
+ state,
3079
+ annotationMap,
3080
+ onSuggestionClick,
3081
+ onLinkOpen,
3082
+ onDismiss,
3083
+ onPopoverEnter,
3084
+ renderPopoverContent,
3085
+ dir
3086
+ }) {
3087
+ const popoverRef = (0, import_react4.useRef)(null);
3088
+ const popperRef = (0, import_react4.useRef)(null);
3089
+ (0, import_react4.useLayoutEffect)(() => {
3090
+ const el = popoverRef.current;
3091
+ if (!el) {
3092
+ popperRef.current?.destroy();
3093
+ popperRef.current = null;
3094
+ return;
3095
+ }
3096
+ const ar = state.anchorRect;
3097
+ const virtualEl = {
3098
+ getBoundingClientRect: () => ({
3099
+ top: ar?.top ?? state.y,
3100
+ left: ar?.left ?? state.x,
3101
+ bottom: ar?.bottom ?? state.y,
3102
+ right: ar?.right ?? state.x,
3103
+ width: ar?.width ?? 0,
3104
+ height: ar?.height ?? 0,
3105
+ x: ar?.left ?? state.x,
3106
+ y: ar?.top ?? state.y,
3107
+ toJSON: () => {
3108
+ }
3109
+ })
3110
+ };
3111
+ el.style.visibility = "hidden";
3112
+ if (popperRef.current) {
3113
+ popperRef.current.state.elements.reference = virtualEl;
3114
+ } else {
3115
+ popperRef.current = (0, import_core2.createPopper)(virtualEl, el, {
3116
+ strategy: "fixed",
3117
+ placement: "bottom-start",
3118
+ modifiers: [
3119
+ { name: "offset", options: { offset: [0, 6] } },
3120
+ { name: "preventOverflow", options: { padding: 16 } },
3121
+ { name: "flip", options: { padding: 16 } }
3122
+ ]
3123
+ });
3124
+ }
3125
+ popperRef.current.forceUpdate();
3126
+ el.style.visibility = "";
3127
+ }, [state.visible, state.x, state.y, state.anchorRect]);
3128
+ (0, import_react4.useLayoutEffect)(() => {
3129
+ return () => {
3130
+ popperRef.current?.destroy();
3131
+ popperRef.current = null;
3132
+ };
3133
+ }, []);
3134
+ if (!state.visible) return null;
3135
+ const annotations = state.ruleIds.map((id) => annotationMap.get(id)).filter((a) => a != null);
3136
+ if (annotations.length === 0) return null;
3137
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3138
+ "div",
3139
+ {
3140
+ ref: popoverRef,
3141
+ className: "cat-popover",
3142
+ dir,
3143
+ style: {
3144
+ position: "fixed",
3145
+ left: 0,
3146
+ top: 0,
3147
+ zIndex: 1e3,
3148
+ visibility: "hidden"
3149
+ },
3150
+ onMouseEnter: () => onPopoverEnter(),
3151
+ onMouseLeave: () => onDismiss(),
3152
+ children: annotations.map((ann, i) => {
3153
+ const custom = renderPopoverContent?.({
3154
+ annotation: ann,
3155
+ onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
3156
+ });
3157
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(React2.Fragment, { children: [
3158
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("hr", { className: "border-border my-0" }),
3159
+ custom != null ? custom : ann.type === "spellcheck" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3160
+ SpellCheckPopoverContent,
3161
+ {
3162
+ data: ann.data,
3163
+ onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
3164
+ }
3165
+ ) : ann.type === "glossary" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(KeywordsPopoverContent, { data: ann.data }) : ann.type === "tag" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TagPopoverContent, { data: ann.data }) : ann.type === "quote" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QuotePopoverContent, { data: ann.data }) : ann.type === "link" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3166
+ LinkPopoverContent,
3167
+ {
3168
+ data: ann.data,
3169
+ onOpen: () => {
3170
+ if (onLinkOpen) {
3171
+ onLinkOpen(ann.data.url);
3172
+ } else {
3173
+ window.open(ann.data.url, "_blank", "noopener,noreferrer");
3174
+ }
3175
+ }
3176
+ }
3177
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SpecialCharPopoverContent, { data: ann.data })
3178
+ ] }, ann.id);
3179
+ })
3180
+ }
3181
+ );
3182
+ }
3183
+
3184
+ // src/layout/cat-editor/CATEditor.tsx
3185
+ var import_jsx_runtime4 = require("react/jsx-runtime");
3186
+ var CATEditor = (0, import_react5.forwardRef)(
3187
+ function CATEditor2({
3188
+ initialText = "",
3189
+ rules = [],
3190
+ onChange,
3191
+ onSuggestionApply,
3192
+ codepointDisplayMap,
3193
+ renderPopoverContent,
3194
+ onLinkClick,
3195
+ openLinksOnClick = true,
3196
+ onMentionClick,
3197
+ onMentionInsert,
3198
+ mentionSerialize,
3199
+ mentionPattern,
3200
+ renderMentionDOM,
3201
+ placeholder = "Start typing or paste text here\u2026",
3202
+ className,
3203
+ dir,
3204
+ popoverDir: popoverDirProp = "ltr",
3205
+ jpFont = false,
3206
+ editable: editableProp,
3207
+ readOnlySelectable = false,
3208
+ onKeyDown: onKeyDownProp,
3209
+ readOnly: readOnlyLegacy = false
3210
+ }, ref) {
3211
+ const isEditable = editableProp !== void 0 ? editableProp : !readOnlyLegacy;
3212
+ const annotationMapRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
3213
+ const editorRef = (0, import_react5.useRef)(null);
3214
+ const savedSelectionRef = (0, import_react5.useRef)(null);
3215
+ const containerRef = (0, import_react5.useRef)(null);
3216
+ const dismissTimerRef = (0, import_react5.useRef)(null);
3217
+ (0, import_react5.useEffect)(() => {
3218
+ setMentionNodeConfig({
3219
+ renderDOM: renderMentionDOM,
3220
+ serialize: mentionSerialize,
3221
+ pattern: mentionPattern
3222
+ });
3223
+ }, [renderMentionDOM, mentionSerialize, mentionPattern]);
3224
+ const flashIdRef = (0, import_react5.useRef)(null);
3225
+ const flashTimerRef = (0, import_react5.useRef)(null);
3226
+ const flashEditUnregRef = (0, import_react5.useRef)(null);
3227
+ const applyFlashClass = (0, import_react5.useCallback)((annotationId) => {
3228
+ const container = containerRef.current;
3229
+ if (!container) return;
3230
+ container.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
3231
+ container.querySelectorAll(".cat-highlight").forEach((el) => {
3232
+ const ids = el.getAttribute("data-rule-ids");
3233
+ if (ids && ids.split(",").includes(annotationId)) {
3234
+ el.classList.add("cat-highlight-flash");
3235
+ }
3236
+ });
3237
+ }, []);
3238
+ const clearFlashInner = (0, import_react5.useCallback)(() => {
3239
+ flashIdRef.current = null;
3240
+ if (flashTimerRef.current) {
3241
+ clearTimeout(flashTimerRef.current);
3242
+ flashTimerRef.current = null;
3243
+ }
3244
+ if (flashEditUnregRef.current) {
3245
+ flashEditUnregRef.current();
3246
+ flashEditUnregRef.current = null;
3247
+ }
3248
+ containerRef.current?.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
3249
+ }, []);
3250
+ const [popoverState, setPopoverState] = (0, import_react5.useState)({
3251
+ visible: false,
3252
+ x: 0,
3253
+ y: 0,
3254
+ ruleIds: []
3255
+ });
3256
+ (0, import_react5.useImperativeHandle)(
3257
+ ref,
3258
+ () => ({
3259
+ insertText: (text) => {
3260
+ const editor = editorRef.current;
3261
+ if (!editor) return;
3262
+ editor.update(() => {
3263
+ const saved = savedSelectionRef.current;
3264
+ if (saved) {
3265
+ const anchorPt = $globalOffsetToPoint(saved.anchor);
3266
+ const focusPt = $globalOffsetToPoint(saved.focus);
3267
+ if (anchorPt && focusPt) {
3268
+ const anchorNode = (0, import_lexical6.$getNodeByKey)(anchorPt.key);
3269
+ if (anchorNode && $isHighlightNode(anchorNode) && anchorNode.getMode() === "token" && saved.anchor === saved.focus) {
3270
+ const newText = (0, import_lexical6.$createTextNode)(text);
3271
+ if (anchorPt.offset === 0 || anchorPt.offset < anchorNode.getTextContentSize()) {
3272
+ anchorNode.insertBefore(newText);
3273
+ } else {
3274
+ anchorNode.insertAfter(newText);
3275
+ }
3276
+ newText.selectEnd();
3277
+ return;
3278
+ }
3279
+ const sel2 = (0, import_lexical6.$createRangeSelection)();
3280
+ sel2.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
3281
+ sel2.focus.set(focusPt.key, focusPt.offset, focusPt.type);
3282
+ (0, import_lexical6.$setSelection)(sel2);
3283
+ sel2.insertText(text);
3284
+ return;
3285
+ }
3286
+ }
3287
+ const root = (0, import_lexical6.$getRoot)();
3288
+ const lastChild = root.getLastChild();
3289
+ if (lastChild) {
3290
+ lastChild.selectEnd();
3291
+ }
3292
+ const sel = (0, import_lexical6.$createRangeSelection)();
3293
+ (0, import_lexical6.$setSelection)(sel);
3294
+ sel.insertText(text);
3295
+ });
3296
+ editor.focus();
3297
+ },
3298
+ focus: () => {
3299
+ editorRef.current?.focus();
3300
+ },
3301
+ getText: () => {
3302
+ let text = "";
3303
+ editorRef.current?.getEditorState().read(() => {
3304
+ text = (0, import_lexical6.$getRoot)().getTextContent();
3305
+ });
3306
+ return text;
3307
+ },
3308
+ flashHighlight: (annotationId, durationMs = 5e3) => {
3309
+ clearFlashInner();
3310
+ flashIdRef.current = annotationId;
3311
+ applyFlashClass(annotationId);
3312
+ flashTimerRef.current = setTimeout(() => {
3313
+ clearFlashInner();
3314
+ }, durationMs);
3315
+ const editor = editorRef.current;
3316
+ if (editor) {
3317
+ flashEditUnregRef.current = editor.registerUpdateListener(
3318
+ ({ tags }) => {
3319
+ if (tags.has("cat-highlights")) {
3320
+ if (flashIdRef.current) {
3321
+ requestAnimationFrame(
3322
+ () => applyFlashClass(flashIdRef.current)
3323
+ );
3324
+ }
3325
+ return;
3326
+ }
3327
+ clearFlashInner();
3328
+ }
3329
+ );
3330
+ }
3331
+ },
3332
+ replaceAll: (search, replacement) => {
3333
+ const editor = editorRef.current;
3334
+ if (!editor || !search) return 0;
3335
+ let count = 0;
3336
+ editor.update(() => {
3337
+ const root = (0, import_lexical6.$getRoot)();
3338
+ const fullText = root.getTextContent();
3339
+ let idx = 0;
3340
+ while ((idx = fullText.indexOf(search, idx)) !== -1) {
3341
+ count++;
3342
+ idx += search.length;
3343
+ }
3344
+ if (count === 0) return;
3345
+ const newText = fullText.split(search).join(replacement);
3346
+ root.clear();
3347
+ const lines = newText.split("\n");
3348
+ for (const line of lines) {
3349
+ const p = (0, import_lexical6.$createParagraphNode)();
3350
+ p.append((0, import_lexical6.$createTextNode)(line));
3351
+ root.append(p);
3352
+ }
3353
+ });
3354
+ return count;
3355
+ },
3356
+ clearFlash: () => {
3357
+ clearFlashInner();
3358
+ }
3359
+ }),
3360
+ [applyFlashClass, clearFlashInner]
3361
+ );
3362
+ const initialConfig = (0, import_react5.useMemo)(
3363
+ () => ({
3364
+ namespace: "CATEditor",
3365
+ theme: {
3366
+ root: "cat-editor-root",
3367
+ paragraph: "cat-editor-paragraph",
3368
+ text: {
3369
+ base: "cat-editor-text"
3370
+ }
3371
+ },
3372
+ nodes: [HighlightNode, MentionNode],
3373
+ // When readOnlySelectable, Lexical must be editable so the caret
3374
+ // and selection work — we block mutations via KEY_DOWN_COMMAND.
3375
+ editable: isEditable || readOnlySelectable,
3376
+ onError: (error) => {
3377
+ console.error("CATEditor Lexical error:", error);
3378
+ },
3379
+ editorState: () => {
3380
+ const root = (0, import_lexical6.$getRoot)();
3381
+ const lines = initialText.split("\n");
3382
+ for (const line of lines) {
3383
+ const p = (0, import_lexical6.$createParagraphNode)();
3384
+ p.append((0, import_lexical6.$createTextNode)(line));
3385
+ root.append(p);
3386
+ }
3387
+ }
3388
+ }),
3389
+ []
3390
+ // intentional: initialConfig should not change
3391
+ );
3392
+ const isOverHighlightRef = (0, import_react5.useRef)(false);
3393
+ const isOverPopoverRef = (0, import_react5.useRef)(false);
3394
+ const scheduleHide = (0, import_react5.useCallback)(() => {
3395
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
3396
+ dismissTimerRef.current = setTimeout(() => {
3397
+ if (!isOverHighlightRef.current && !isOverPopoverRef.current) {
3398
+ setPopoverState((prev) => ({ ...prev, visible: false }));
3399
+ }
3400
+ }, 400);
3401
+ }, []);
3402
+ const cancelHide = (0, import_react5.useCallback)(() => {
3403
+ if (dismissTimerRef.current) {
3404
+ clearTimeout(dismissTimerRef.current);
3405
+ dismissTimerRef.current = null;
3406
+ }
3407
+ }, []);
3408
+ (0, import_react5.useEffect)(() => {
3409
+ const container = containerRef.current;
3410
+ if (!container) return;
3411
+ const handleMouseOver = (e) => {
3412
+ const target = e.target.closest(".cat-highlight");
3413
+ if (!target) {
3414
+ if (isOverHighlightRef.current) {
3415
+ isOverHighlightRef.current = false;
3416
+ scheduleHide();
3417
+ }
3418
+ return;
3419
+ }
3420
+ const ruleIdsAttr = target.getAttribute("data-rule-ids");
3421
+ if (!ruleIdsAttr) return;
3422
+ const ruleIds = [
3423
+ ...new Set(
3424
+ ruleIdsAttr.split(",").map(
3425
+ (id) => id.startsWith(NL_MARKER_PREFIX) ? id.slice(NL_MARKER_PREFIX.length) : id
3426
+ )
3427
+ )
3428
+ ];
3429
+ isOverHighlightRef.current = true;
3430
+ cancelHide();
3431
+ const rect = target.getBoundingClientRect();
3432
+ setPopoverState({
3433
+ visible: true,
3434
+ x: rect.left,
3435
+ y: rect.bottom,
3436
+ anchorRect: {
3437
+ top: rect.top,
3438
+ left: rect.left,
3439
+ bottom: rect.bottom,
3440
+ right: rect.right,
3441
+ width: rect.width,
3442
+ height: rect.height
3443
+ },
3444
+ ruleIds
3445
+ });
3446
+ };
3447
+ const handleMouseOut = (e) => {
3448
+ const related = e.relatedTarget;
3449
+ if (related?.closest(".cat-highlight")) return;
3450
+ isOverHighlightRef.current = false;
3451
+ scheduleHide();
3452
+ };
3453
+ container.addEventListener("mouseover", handleMouseOver);
3454
+ container.addEventListener("mouseout", handleMouseOut);
3455
+ return () => {
3456
+ container.removeEventListener("mouseover", handleMouseOver);
3457
+ container.removeEventListener("mouseout", handleMouseOut);
3458
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
3459
+ };
3460
+ }, [scheduleHide, cancelHide]);
3461
+ (0, import_react5.useEffect)(() => {
3462
+ const container = containerRef.current;
3463
+ if (!container) return;
3464
+ const handleClick = (e) => {
3465
+ if (openLinksOnClick) {
3466
+ const highlightTarget = e.target.closest(
3467
+ ".cat-highlight"
3468
+ );
3469
+ if (highlightTarget) {
3470
+ const ruleIdsAttr = highlightTarget.getAttribute("data-rule-ids");
3471
+ if (ruleIdsAttr) {
3472
+ const ids = ruleIdsAttr.split(",");
3473
+ for (const id of ids) {
3474
+ const ann = annotationMapRef.current.get(id);
3475
+ if (!ann) continue;
3476
+ if (ann.type === "link") {
3477
+ e.preventDefault();
3478
+ if (onLinkClick) {
3479
+ onLinkClick(ann.data.url);
3480
+ } else {
3481
+ window.open(ann.data.url, "_blank", "noopener,noreferrer");
3482
+ }
3483
+ return;
3484
+ }
3485
+ }
3486
+ }
3487
+ }
3488
+ }
3489
+ const mentionTarget = e.target.closest(
3490
+ ".cat-mention-node"
3491
+ );
3492
+ if (mentionTarget) {
3493
+ const mentionId = mentionTarget.getAttribute("data-mention-id");
3494
+ const mentionName = mentionTarget.getAttribute("data-mention-name");
3495
+ if (mentionId && mentionName) {
3496
+ e.preventDefault();
3497
+ onMentionClick?.(mentionId, mentionName);
3498
+ return;
3499
+ }
3500
+ }
3501
+ };
3502
+ container.addEventListener("click", handleClick);
3503
+ return () => {
3504
+ container.removeEventListener("click", handleClick);
3505
+ };
3506
+ }, [onLinkClick, openLinksOnClick, onMentionClick]);
3507
+ const handleSuggestionClick = (0, import_react5.useCallback)(
3508
+ (suggestion, ruleId) => {
3509
+ const editor = editorRef.current;
3510
+ if (!editor) return;
3511
+ let replacedRange;
3512
+ editor.update(() => {
3513
+ const root = (0, import_lexical6.$getRoot)();
3514
+ const allNodes = root.getAllTextNodes();
3515
+ for (const node of allNodes) {
3516
+ if ($isHighlightNode(node) && node.__ruleIds.split(",").includes(ruleId)) {
3517
+ const globalOffset = $pointToGlobalOffset(node.getKey(), 0);
3518
+ const originalContent = node.getTextContent();
3519
+ replacedRange = {
3520
+ start: globalOffset,
3521
+ end: globalOffset + originalContent.length,
3522
+ content: originalContent
3523
+ };
3524
+ const textNode = (0, import_lexical6.$createTextNode)(suggestion);
3525
+ node.replace(textNode);
3526
+ break;
3527
+ }
3528
+ }
3529
+ });
3530
+ setPopoverState((prev) => ({ ...prev, visible: false }));
3531
+ if (replacedRange !== void 0) {
3532
+ const annotation = annotationMapRef.current.get(ruleId);
3533
+ if (annotation) {
3534
+ onSuggestionApply?.(
3535
+ ruleId,
3536
+ suggestion,
3537
+ replacedRange,
3538
+ annotation.type
3539
+ );
3540
+ }
3541
+ }
3542
+ },
3543
+ [onSuggestionApply]
3544
+ );
3545
+ const handleChange = (0, import_react5.useCallback)(
3546
+ (editorState) => {
3547
+ if (!onChange) return;
3548
+ editorState.read(() => {
3549
+ const root = (0, import_lexical6.$getRoot)();
3550
+ onChange(root.getTextContent());
3551
+ });
3552
+ },
3553
+ [onChange]
3554
+ );
3555
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3556
+ "div",
3557
+ {
3558
+ ref: containerRef,
3559
+ className: cn(
3560
+ "cat-editor-container",
3561
+ jpFont && "cat-editor-jp-font",
3562
+ className
3563
+ ),
3564
+ dir,
3565
+ children: [
3566
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_LexicalComposer.LexicalComposer, { initialConfig, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "cat-editor-inner", children: [
3567
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3568
+ import_LexicalPlainTextPlugin.PlainTextPlugin,
3569
+ {
3570
+ contentEditable: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3571
+ import_LexicalContentEditable.ContentEditable,
3572
+ {
3573
+ className: cn(
3574
+ "cat-editor-editable",
3575
+ !isEditable && !readOnlySelectable && "cat-editor-readonly",
3576
+ !isEditable && readOnlySelectable && "cat-editor-readonly-selectable"
3577
+ )
3578
+ }
3579
+ ),
3580
+ placeholder: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "cat-editor-placeholder", children: placeholder }),
3581
+ ErrorBoundary: import_LexicalErrorBoundary.LexicalErrorBoundary
3582
+ }
3583
+ ),
3584
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_LexicalHistoryPlugin.HistoryPlugin, {}),
3585
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange }),
3586
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3587
+ HighlightsPlugin,
3588
+ {
3589
+ rules,
3590
+ annotationMapRef,
3591
+ codepointDisplayMap
3592
+ }
3593
+ ),
3594
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3595
+ EditorRefPlugin,
3596
+ {
3597
+ editorRef,
3598
+ savedSelectionRef
3599
+ }
3600
+ ),
3601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NLMarkerNavigationPlugin, {}),
3602
+ !isEditable && readOnlySelectable && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ReadOnlySelectablePlugin, {}),
3603
+ onKeyDownProp && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyDownPlugin, { onKeyDown: onKeyDownProp }),
3604
+ dir && dir !== "auto" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DirectionPlugin, { dir }),
3605
+ rules.filter((r) => r.type === "mention").map((mentionRule, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3606
+ MentionPlugin,
3607
+ {
3608
+ users: mentionRule.users,
3609
+ trigger: mentionRule.trigger,
3610
+ onMentionInsert
3611
+ },
3612
+ `mention-${i}`
3613
+ ))
3614
+ ] }) }),
3615
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3616
+ HighlightPopover,
3617
+ {
3618
+ state: popoverState,
3619
+ annotationMap: annotationMapRef.current,
3620
+ onSuggestionClick: handleSuggestionClick,
3621
+ onLinkOpen: onLinkClick,
3622
+ onDismiss: () => {
3623
+ isOverPopoverRef.current = false;
3624
+ scheduleHide();
3625
+ },
3626
+ onPopoverEnter: () => {
3627
+ isOverPopoverRef.current = true;
3628
+ cancelHide();
3629
+ },
3630
+ renderPopoverContent,
3631
+ dir: popoverDirProp === "inherit" ? dir : popoverDirProp
3632
+ }
3633
+ )
3634
+ ]
3635
+ }
3636
+ );
3637
+ }
3638
+ );
3639
+ function ReadOnlySelectablePlugin() {
3640
+ const [editor] = (0, import_LexicalComposerContext3.useLexicalComposerContext)();
3641
+ (0, import_react5.useEffect)(() => {
3642
+ return editor.registerCommand(
3643
+ import_lexical6.KEY_DOWN_COMMAND,
3644
+ (event) => {
3645
+ 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
3646
+ (event.ctrlKey || event.metaKey) && (event.key === "c" || event.key === "a" || event.key === "f" || event.key === "g")) {
3647
+ return false;
3648
+ }
3649
+ event.preventDefault();
3650
+ return true;
3651
+ },
3652
+ import_lexical6.COMMAND_PRIORITY_CRITICAL
3653
+ );
3654
+ }, [editor]);
3655
+ (0, import_react5.useEffect)(() => {
3656
+ const root = editor.getRootElement();
3657
+ if (!root) return;
3658
+ const block = (e) => e.preventDefault();
3659
+ root.addEventListener("paste", block);
3660
+ root.addEventListener("cut", block);
3661
+ root.addEventListener("drop", block);
3662
+ return () => {
3663
+ root.removeEventListener("paste", block);
3664
+ root.removeEventListener("cut", block);
3665
+ root.removeEventListener("drop", block);
3666
+ };
3667
+ }, [editor]);
3668
+ return null;
3669
+ }
3670
+ function KeyDownPlugin({
3671
+ onKeyDown
3672
+ }) {
3673
+ const [editor] = (0, import_LexicalComposerContext3.useLexicalComposerContext)();
3674
+ (0, import_react5.useEffect)(() => {
3675
+ return editor.registerCommand(
3676
+ import_lexical6.KEY_DOWN_COMMAND,
3677
+ (event) => {
3678
+ return onKeyDown(event);
3679
+ },
3680
+ import_lexical6.COMMAND_PRIORITY_CRITICAL
3681
+ );
3682
+ }, [editor, onKeyDown]);
3683
+ return null;
3684
+ }
3685
+ function DirectionPlugin({ dir }) {
3686
+ const [editor] = (0, import_LexicalComposerContext3.useLexicalComposerContext)();
3687
+ (0, import_react5.useEffect)(() => {
3688
+ editor.update(() => {
3689
+ (0, import_lexical6.$getRoot)().setDirection(dir);
3690
+ });
3691
+ return () => {
3692
+ editor.update(() => {
3693
+ (0, import_lexical6.$getRoot)().setDirection(null);
3694
+ });
3695
+ };
3696
+ }, [editor, dir]);
3697
+ return null;
3698
+ }
1093
3699
  // Annotate the CommonJS export names for ESM import in node:
1094
3700
  0 && (module.exports = {
3701
+ BUILTIN_ESCAPE_PATTERNS,
3702
+ CATEditor,
1095
3703
  LayoutSelect,
1096
- LayoutSelectDefault
3704
+ LayoutSelectDefault,
3705
+ detectQuotes
1097
3706
  });
1098
3707
  //# sourceMappingURL=index.cjs.map