@teriusu/rich-editor 0.10.8 → 0.11.0

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 (147) hide show
  1. package/dist/components/BubbleToolbar.svelte +40 -36
  2. package/dist/components/BubbleToolbar.svelte.d.ts +3 -0
  3. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  4. package/dist/components/EmojiSuggestionMenu.svelte +291 -0
  5. package/dist/components/EmojiSuggestionMenu.svelte.d.ts +9 -0
  6. package/dist/components/EmojiSuggestionMenu.svelte.d.ts.map +1 -0
  7. package/dist/components/FixedToolbar.svelte +53 -47
  8. package/dist/components/FixedToolbar.svelte.d.ts +3 -0
  9. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  10. package/dist/components/FloatingHelper.svelte +219 -0
  11. package/dist/components/FloatingHelper.svelte.d.ts +14 -0
  12. package/dist/components/FloatingHelper.svelte.d.ts.map +1 -0
  13. package/dist/components/InputModal.svelte +7 -3
  14. package/dist/components/InputModal.svelte.d.ts +3 -0
  15. package/dist/components/InputModal.svelte.d.ts.map +1 -1
  16. package/dist/components/MathModal.svelte +33 -29
  17. package/dist/components/MathModal.svelte.d.ts +3 -0
  18. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  19. package/dist/components/MediaPickerModal.svelte +7 -3
  20. package/dist/components/MediaPickerModal.svelte.d.ts +3 -0
  21. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -1
  22. package/dist/components/SlashCommandMenu.svelte +88 -72
  23. package/dist/components/SlashCommandMenu.svelte.d.ts +3 -0
  24. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  25. package/dist/components/TableBubbleMenu.svelte +41 -33
  26. package/dist/components/TableBubbleMenu.svelte.d.ts +3 -0
  27. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  28. package/dist/components/TipTapEditor.svelte +239 -127
  29. package/dist/components/TipTapEditor.svelte.d.ts +7 -0
  30. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  31. package/dist/extensions/CardBlock.d.ts.map +1 -1
  32. package/dist/extensions/CardBlock.js +6 -4
  33. package/dist/extensions/FileAttachment.d.ts.map +1 -1
  34. package/dist/extensions/FileAttachment.js +2 -1
  35. package/dist/extensions/FixedDetails.d.ts.map +1 -1
  36. package/dist/extensions/FixedDetails.js +5 -3
  37. package/dist/extensions/LegacyBlock.d.ts.map +1 -1
  38. package/dist/extensions/LegacyBlock.js +8 -6
  39. package/dist/extensions/Math.d.ts.map +1 -1
  40. package/dist/extensions/Math.js +5 -2
  41. package/dist/extensions/MbusVideo.d.ts.map +1 -1
  42. package/dist/extensions/MbusVideo.js +73 -9
  43. package/dist/extensions/MediaToolbar.d.ts +41 -0
  44. package/dist/extensions/MediaToolbar.d.ts.map +1 -0
  45. package/dist/extensions/MediaToolbar.js +196 -0
  46. package/dist/extensions/MediaToolbar.test.d.ts +2 -0
  47. package/dist/extensions/MediaToolbar.test.d.ts.map +1 -0
  48. package/dist/extensions/MediaToolbar.test.js +231 -0
  49. package/dist/extensions/OrderedListMarker.d.ts +2 -0
  50. package/dist/extensions/OrderedListMarker.d.ts.map +1 -0
  51. package/dist/extensions/OrderedListMarker.js +114 -0
  52. package/dist/extensions/OrderedListMarker.test.d.ts +2 -0
  53. package/dist/extensions/OrderedListMarker.test.d.ts.map +1 -0
  54. package/dist/extensions/OrderedListMarker.test.js +176 -0
  55. package/dist/extensions/PdfBlock.d.ts.map +1 -1
  56. package/dist/extensions/PdfBlock.js +25 -23
  57. package/dist/extensions/ResizableImage.d.ts.map +1 -1
  58. package/dist/extensions/ResizableImage.js +20 -1
  59. package/dist/extensions/TableKeymap.d.ts +16 -0
  60. package/dist/extensions/TableKeymap.d.ts.map +1 -0
  61. package/dist/extensions/TableKeymap.js +97 -0
  62. package/dist/extensions/TableKeymap.test.d.ts +2 -0
  63. package/dist/extensions/TableKeymap.test.d.ts.map +1 -0
  64. package/dist/extensions/TableKeymap.test.js +147 -0
  65. package/dist/extensions/TabsBlock.d.ts.map +1 -1
  66. package/dist/extensions/TabsBlock.js +12 -9
  67. package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
  68. package/dist/extensions/TiptapMidibus.js +60 -6
  69. package/dist/extensions/UploadSkeleton.d.ts +61 -0
  70. package/dist/extensions/UploadSkeleton.d.ts.map +1 -0
  71. package/dist/extensions/UploadSkeleton.js +233 -0
  72. package/dist/extensions/UploadSkeleton.test.d.ts +2 -0
  73. package/dist/extensions/UploadSkeleton.test.d.ts.map +1 -0
  74. package/dist/extensions/UploadSkeleton.test.js +128 -0
  75. package/dist/extensions/VideoEmbed.d.ts +8 -0
  76. package/dist/extensions/VideoEmbed.d.ts.map +1 -1
  77. package/dist/extensions/VideoEmbed.js +226 -45
  78. package/dist/extensions/VideoEmbed.test.js +131 -2
  79. package/dist/i18n/en.d.ts +3 -0
  80. package/dist/i18n/en.d.ts.map +1 -0
  81. package/dist/i18n/en.js +191 -0
  82. package/dist/i18n/es.d.ts +3 -0
  83. package/dist/i18n/es.d.ts.map +1 -0
  84. package/dist/i18n/es.js +191 -0
  85. package/dist/i18n/index.d.ts +43 -0
  86. package/dist/i18n/index.d.ts.map +1 -0
  87. package/dist/i18n/index.js +109 -0
  88. package/dist/i18n/index.test.d.ts +2 -0
  89. package/dist/i18n/index.test.d.ts.map +1 -0
  90. package/dist/i18n/index.test.js +173 -0
  91. package/dist/i18n/ja.d.ts +3 -0
  92. package/dist/i18n/ja.d.ts.map +1 -0
  93. package/dist/i18n/ja.js +191 -0
  94. package/dist/i18n/ko.d.ts +198 -0
  95. package/dist/i18n/ko.d.ts.map +1 -0
  96. package/dist/i18n/ko.js +215 -0
  97. package/dist/i18n/zh-hans.d.ts +3 -0
  98. package/dist/i18n/zh-hans.d.ts.map +1 -0
  99. package/dist/i18n/zh-hans.js +191 -0
  100. package/dist/i18n/zh-hant.d.ts +3 -0
  101. package/dist/i18n/zh-hant.d.ts.map +1 -0
  102. package/dist/i18n/zh-hant.js +192 -0
  103. package/dist/index.d.ts +7 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -1
  106. package/dist/static/highlight.d.ts +13 -0
  107. package/dist/static/highlight.d.ts.map +1 -0
  108. package/dist/static/highlight.js +99 -0
  109. package/dist/static/index.d.ts +33 -0
  110. package/dist/static/index.d.ts.map +1 -0
  111. package/dist/static/index.js +367 -0
  112. package/dist/static/index.test.d.ts +2 -0
  113. package/dist/static/index.test.d.ts.map +1 -0
  114. package/dist/static/index.test.js +194 -0
  115. package/dist/static/tokenizer.d.ts +52 -0
  116. package/dist/static/tokenizer.d.ts.map +1 -0
  117. package/dist/static/tokenizer.js +289 -0
  118. package/dist/styles/editor.css +1 -1
  119. package/dist/utils/emoji-data.d.ts +15 -0
  120. package/dist/utils/emoji-suggestion.d.ts +48 -0
  121. package/dist/utils/emoji-suggestion.d.ts.map +1 -0
  122. package/dist/utils/emoji-suggestion.js +51 -0
  123. package/dist/utils/emoji-suggestion.test.d.ts +2 -0
  124. package/dist/utils/emoji-suggestion.test.d.ts.map +1 -0
  125. package/dist/utils/emoji-suggestion.test.js +117 -0
  126. package/dist/utils/floating-helper.d.ts +60 -0
  127. package/dist/utils/floating-helper.d.ts.map +1 -0
  128. package/dist/utils/floating-helper.js +63 -0
  129. package/dist/utils/floating-helper.test.d.ts +2 -0
  130. package/dist/utils/floating-helper.test.d.ts.map +1 -0
  131. package/dist/utils/floating-helper.test.js +141 -0
  132. package/dist/utils/guard-vscode-paste.d.ts +19 -0
  133. package/dist/utils/guard-vscode-paste.d.ts.map +1 -0
  134. package/dist/utils/guard-vscode-paste.js +46 -0
  135. package/dist/utils/guard-vscode-paste.test.d.ts +2 -0
  136. package/dist/utils/guard-vscode-paste.test.d.ts.map +1 -0
  137. package/dist/utils/guard-vscode-paste.test.js +86 -0
  138. package/dist/utils/media-size.d.ts +29 -0
  139. package/dist/utils/media-size.d.ts.map +1 -0
  140. package/dist/utils/media-size.js +90 -0
  141. package/dist/utils/resize.d.ts +12 -0
  142. package/dist/utils/resize.d.ts.map +1 -1
  143. package/dist/utils/resize.js +5 -5
  144. package/dist/utils/sanitize.d.ts +2 -0
  145. package/dist/utils/sanitize.d.ts.map +1 -1
  146. package/dist/utils/sanitize.js +28 -3
  147. package/package.json +8 -1
@@ -2,6 +2,7 @@ import { Node as TiptapNode, mergeAttributes } from "@tiptap/core";
2
2
  import { NodeSelection } from "@tiptap/pm/state";
3
3
  import { getPdfJs } from "../utils/pdf";
4
4
  import { attachResize } from "../utils/resize";
5
+ import { getEditorTranslator } from "../i18n";
5
6
  /**
6
7
  * 방향키 → 쪽 넘김. 노드뷰가 쪽 상태를 쥐고 있으므로 **이벤트로 건넨다** —
7
8
  * 확장이 노드뷰 내부를 직접 들여다보지 않게 하는 게 목적이다.
@@ -90,6 +91,7 @@ export const PdfBlock = TiptapNode.create({
90
91
  },
91
92
  addNodeView() {
92
93
  return ({ node, getPos, editor }) => {
94
+ const t = getEditorTranslator(editor);
93
95
  let destroyed = false;
94
96
  let resizeObserver = null;
95
97
  let resizeTimeout;
@@ -123,7 +125,7 @@ export const PdfBlock = TiptapNode.create({
123
125
  */
124
126
  frame.tabIndex = 0;
125
127
  frame.setAttribute("role", "group");
126
- frame.setAttribute("aria-label", "PDF 문서 — 좌우 방향키로 쪽 넘김");
128
+ frame.setAttribute("aria-label", t("pdfViewerAria"));
127
129
  dom.appendChild(frame);
128
130
  // 리사이즈 핸들 (편집 가능 모드에서만)
129
131
  if (editor.isEditable) {
@@ -133,7 +135,7 @@ export const PdfBlock = TiptapNode.create({
133
135
  getPos,
134
136
  getNode: () => currentNode,
135
137
  axis: "x",
136
- label: "PDF 너비 조절",
138
+ label: t("pdfResizeWidth"),
137
139
  });
138
140
  }
139
141
  /*
@@ -213,10 +215,10 @@ export const PdfBlock = TiptapNode.create({
213
215
  * 쉬운데 실제로는 페이지 원본 대비다 — 호버 설명에서 그걸 먼저 말한다.
214
216
  */
215
217
  const ZOOM_TIPS = {
216
- 0.5: "원본 크기의 50% — 썸네일. 글자는 읽기 어렵습니다",
217
- 0.75: "원본 크기의 75% — 그림·도표 확인용",
218
- 1: "원본 크기 그대로 (100%)",
219
- 1.5: "원본 크기의 150% — 읽기 편한 크기",
218
+ 0.5: t("pdfZoom50"),
219
+ 0.75: t("pdfZoom75"),
220
+ 1: t("pdfZoom100"),
221
+ 1.5: t("pdfZoom150"),
220
222
  };
221
223
  /** 저장값이 이것이면 창 높이에 맞춰 푼다. */
222
224
  const FIT_SCREEN = "fit";
@@ -275,7 +277,7 @@ export const PdfBlock = TiptapNode.create({
275
277
  const btn = document.createElement("button");
276
278
  btn.type = "button";
277
279
  btn.className = PRESET_IDLE;
278
- btn.dataset.tip = ZOOM_TIPS[zoom] ?? `원본 크기의 ${label}`;
280
+ btn.dataset.tip = ZOOM_TIPS[zoom] ?? t("pdfZoomOther", { label });
279
281
  btn.setAttribute("aria-label", btn.dataset.tip);
280
282
  btn.textContent = label;
281
283
  // 원본 폭을 알기 전(로딩 중)에는 누를 수 없다 — 계산할 근거가 없다.
@@ -307,12 +309,12 @@ export const PdfBlock = TiptapNode.create({
307
309
  const FITS = [
308
310
  {
309
311
  value: FIT_WIDTH,
310
- tip: "너비에 맞춤 — 본문 너비를 가득 채웁니다",
312
+ tip: t("pdfFitWidth"),
311
313
  paths: '<polyline points="18 8 22 12 18 16"/><polyline points="6 8 2 12 6 16"/><line x1="2" x2="22" y1="12" y2="12"/>'
312
314
  },
313
315
  {
314
316
  value: FIT_SCREEN,
315
- tip: "화면에 맞춤 — 한 쪽이 화면에 들어오는 크기",
317
+ tip: t("pdfFitPage"),
316
318
  paths: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>'
317
319
  }
318
320
  ];
@@ -372,8 +374,8 @@ export const PdfBlock = TiptapNode.create({
372
374
  downloadLink.rel = "noopener noreferrer";
373
375
  downloadLink.setAttribute("download", cleanName(node.attrs.name));
374
376
  downloadLink.className = "pdf-ctl";
375
- downloadLink.dataset.tip = "\uB2E4\uC6B4\uB85C\uB4DC";
376
- downloadLink.setAttribute("aria-label", "\uB2E4\uC6B4\uB85C\uB4DC");
377
+ downloadLink.dataset.tip = t("download");
378
+ downloadLink.setAttribute("aria-label", t("download"));
377
379
  downloadLink.innerHTML = icon('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>');
378
380
  downloadLink.addEventListener("click", async (e) => {
379
381
  if (!downloadLink.href)
@@ -407,16 +409,16 @@ export const PdfBlock = TiptapNode.create({
407
409
  openLink.target = "_blank";
408
410
  openLink.rel = "noopener noreferrer";
409
411
  openLink.className = "pdf-ctl";
410
- openLink.dataset.tip = "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30 \u2014 \uBE0C\uB77C\uC6B0\uC800 \uBDF0\uC5B4\uB85C \uD06C\uAC8C \uBD05\uB2C8\uB2E4";
411
- openLink.setAttribute("aria-label", "\uC0C8 \uD0ED\uC5D0\uC11C \uC5F4\uAE30");
412
+ openLink.dataset.tip = t("pdfOpenNewTabTip");
413
+ openLink.setAttribute("aria-label", t("pdfOpenNewTab"));
412
414
  openLink.innerHTML = icon('<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>');
413
415
  endCluster.appendChild(openLink);
414
416
  if (editor.isEditable) {
415
417
  const deleteBtn = document.createElement("button");
416
418
  deleteBtn.type = "button";
417
419
  deleteBtn.className = "pdf-ctl pdf-ctl-danger";
418
- deleteBtn.dataset.tip = "\uC0AD\uC81C";
419
- deleteBtn.setAttribute("aria-label", "\uC0AD\uC81C");
420
+ deleteBtn.dataset.tip = t("delete");
421
+ deleteBtn.setAttribute("aria-label", t("delete"));
420
422
  deleteBtn.innerHTML = icon('<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>');
421
423
  deleteBtn.addEventListener("click", () => {
422
424
  const pos = getPos();
@@ -433,7 +435,7 @@ export const PdfBlock = TiptapNode.create({
433
435
  frame.appendChild(canvas);
434
436
  const statusDiv = document.createElement("div");
435
437
  statusDiv.className = "pdf-status";
436
- statusDiv.textContent = "PDF \uB85C\uB529 \uC911...";
438
+ statusDiv.textContent = t("pdfLoading");
437
439
  frame.appendChild(statusDiv);
438
440
  canvas.style.display = "none";
439
441
  // \uC870\uC791\uBD80\uB294 \uB9C8\uC9C0\uB9C9 \u2014 `position:absolute` \uB77C \uC21C\uC11C\uC640 \uBB34\uAD00\uD558\uAC8C \uC704\uC5D0 \uADF8\uB824\uC9C0\uC9C0\uB9CC,
@@ -443,14 +445,14 @@ export const PdfBlock = TiptapNode.create({
443
445
  const prevBtn = document.createElement("button");
444
446
  prevBtn.type = "button";
445
447
  prevBtn.className = "pdf-ctl pdf-nav pdf-nav-prev pdf-cluster";
446
- prevBtn.title = "\uC774\uC804 \uD398\uC774\uC9C0";
447
- prevBtn.setAttribute("aria-label", "\uC774\uC804 \uD398\uC774\uC9C0");
448
+ prevBtn.title = t("pdfPrevPage");
449
+ prevBtn.setAttribute("aria-label", t("pdfPrevPage"));
448
450
  prevBtn.innerHTML = icon('<path d="m15 18-6-6 6-6"/>');
449
451
  const nextBtn = document.createElement("button");
450
452
  nextBtn.type = "button";
451
453
  nextBtn.className = "pdf-ctl pdf-nav pdf-nav-next pdf-cluster";
452
- nextBtn.title = "\uB2E4\uC74C \uD398\uC774\uC9C0";
453
- nextBtn.setAttribute("aria-label", "\uB2E4\uC74C \uD398\uC774\uC9C0");
454
+ nextBtn.title = t("pdfNextPage");
455
+ nextBtn.setAttribute("aria-label", t("pdfNextPage"));
454
456
  nextBtn.innerHTML = icon('<path d="m9 18 6-6-6-6"/>');
455
457
  /*
456
458
  * \uD310\uC774 \uD654\uBA74\uBCF4\uB2E4 \uAE38 \uB54C \uC77D\uAE30 \uC870\uC791\uBD80\uAC00 \uB530\uB77C\uC624\uAC8C \uD558\uB294 \uB808\uC77C. `position: sticky` \uB77C
@@ -464,7 +466,7 @@ export const PdfBlock = TiptapNode.create({
464
466
  const pageBtn = document.createElement("button");
465
467
  pageBtn.type = "button";
466
468
  pageBtn.className = "pdf-page";
467
- pageBtn.title = "\uCABD \uBC88\uD638\uB85C \uC774\uB3D9";
469
+ pageBtn.title = t("pdfGoToPage");
468
470
  pageCluster.appendChild(pageBtn);
469
471
  /*
470
472
  * \uCABD\uC218\uB97C \uB20C\uB7EC **\uBC88\uD638\uB85C \uAC74\uB108\uB6F4\uB2E4.** \uC774\uAC8C \uC5C6\uC73C\uBA74 30\uCABD\uC9DC\uB9AC \uC790\uB8CC\uC5D0\uC11C \uB4A4\uB85C \uAC00\uB824\uBA74
@@ -474,7 +476,7 @@ export const PdfBlock = TiptapNode.create({
474
476
  pageInput.type = "text";
475
477
  pageInput.inputMode = "numeric";
476
478
  pageInput.className = "pdf-page-input";
477
- pageInput.setAttribute("aria-label", "\uCABD \uBC88\uD638");
479
+ pageInput.setAttribute("aria-label", t("pdfPageNumber"));
478
480
  pageInput.style.display = "none";
479
481
  pageCluster.appendChild(pageInput);
480
482
  let currentPage = 1;
@@ -707,7 +709,7 @@ export const PdfBlock = TiptapNode.create({
707
709
  resizeObserver.observe(dom);
708
710
  }
709
711
  catch {
710
- statusDiv.textContent = "PDF\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
712
+ statusDiv.textContent = t("pdfLoadFailed");
711
713
  }
712
714
  }
713
715
  // URL 해결 후 PDF 로드
@@ -1 +1 @@
1
- {"version":3,"file":"ResizableImage.d.ts","sourceRoot":"","sources":["../../src/extensions/ResizableImage.ts"],"names":[],"mappings":"AAqCA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYjE;AAED,eAAO,MAAM,cAAc,kFAyFzB,CAAC;AAEH,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"ResizableImage.d.ts","sourceRoot":"","sources":["../../src/extensions/ResizableImage.ts"],"names":[],"mappings":"AAuCA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYjE;AAED,eAAO,MAAM,cAAc,kFA2GzB,CAAC;AAEH,eAAe,cAAc,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import Image from "@tiptap/extension-image";
2
2
  import { attachResize } from "../utils/resize";
3
+ import { normalizeMediaAlign } from "../utils/media-size";
4
+ import { getEditorTranslator } from "../i18n";
3
5
  /**
4
6
  * 드래그로 폭을 조절할 수 있는 이미지.
5
7
  *
@@ -74,6 +76,19 @@ export const ResizableImage = Image.extend({
74
76
  const w = normalizeImageWidth(attributes.width);
75
77
  return w == null ? {} : { width: String(w) };
76
78
  }
79
+ },
80
+ /*
81
+ * 좌/중/우 정렬(미디어 툴바). `width` 와 같은 이유로 `style` 이 아니라 **속성**
82
+ * (`data-align`)으로 간다 — 살균기의 `img` 허용 목록에 이 이름을 넣어 두었고,
83
+ * 읽기 화면에서는 `editor.css` 의 `.tiptap img[data-align=…]` 규칙이 받는다.
84
+ */
85
+ align: {
86
+ default: null,
87
+ parseHTML: (element) => normalizeMediaAlign(element.getAttribute("data-align")),
88
+ renderHTML: (attributes) => {
89
+ const align = normalizeMediaAlign(attributes.align);
90
+ return align ? { "data-align": align } : {};
91
+ }
77
92
  }
78
93
  };
79
94
  },
@@ -108,6 +123,10 @@ export const ResizableImage = Image.extend({
108
123
  dom.style.removeProperty("width");
109
124
  else
110
125
  dom.style.width = `${w}px`;
126
+ /* 정렬은 래퍼의 마진으로 — `width: fit-content` 라 `auto` 마진이 그대로 먹는다. */
127
+ const align = normalizeMediaAlign(attrs.align);
128
+ dom.style.marginLeft = align === "center" || align === "right" ? "auto" : "";
129
+ dom.style.marginRight = align === "center" ? "auto" : "";
111
130
  };
112
131
  applyAttrs(node.attrs);
113
132
  if (editor.isEditable) {
@@ -119,7 +138,7 @@ export const ResizableImage = Image.extend({
119
138
  axis: "x",
120
139
  attr: "width",
121
140
  min: MIN_WIDTH,
122
- label: "이미지 너비 조절",
141
+ label: getEditorTranslator(editor)("imageResizeWidth"),
123
142
  // 저장 형식은 단위 없는 숫자 — 살균기가 `style` 을 지우므로 `width` 속성으로 간다.
124
143
  format: (v) => String(Math.round(v))
125
144
  });
@@ -0,0 +1,16 @@
1
+ import { Extension } from "@tiptap/core";
2
+ /**
3
+ * 표 키보드 동작: `Tab` 다음 셀(마지막 셀이면 행을 만들어 이동), `Shift-Tab` 이전 셀,
4
+ * `Backspace`/`Delete` 로 빈 표·선택된 열/행/표 삭제. (main 정올
5
+ * `plugin/table/index.ts` 의 `addKeyboardShortcuts` 참조 — 로직 재작성.)
6
+ *
7
+ * ⚠️ **업스트림 `Table` 에도 같은 Tab 단축키가 있는데 왜 또 다는가** — TipTap 은 키맵
8
+ * 플러그인을 **등록 역순 + priority 내림차순**으로 배치한다(core 의
9
+ * `sortExtensions([...this.extensions].reverse())`). `TipTapEditor.svelte` 에서 `Indent` 가
10
+ * 표보다 **뒤에** 등록돼 있어, 기본 priority 로는 표 안에서도 Indent 의 Tab(NBSP 삽입)이
11
+ * 먼저 먹어 셀 이동이 죽는다. 그래서 여기만 priority 를 올려 표 안에서는 셀 이동이
12
+ * 이기게 한다. 표 밖에서는 모든 핸들러가 false 를 돌려주므로 Indent·리스트 들여쓰기는
13
+ * 그대로다.
14
+ */
15
+ export declare const TableKeymap: Extension<any, any>;
16
+ //# sourceMappingURL=TableKeymap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TableKeymap.d.ts","sourceRoot":"","sources":["../../src/extensions/TableKeymap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAuDzC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,qBAwBtB,CAAC"}
@@ -0,0 +1,97 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { CellSelection } from "@tiptap/pm/tables";
3
+ /**
4
+ * 표 안에 실제 내용(글자·이미지·수식 같은 리프 노드)이 하나도 없는가.
5
+ * 행·셀·빈 문단 같은 구조물만 있으면 빈 표로 본다.
6
+ */
7
+ function isTableEmpty(table) {
8
+ let empty = true;
9
+ table.descendants((node) => {
10
+ if (!empty)
11
+ return false;
12
+ if (node.isText || node.isLeaf) {
13
+ empty = false;
14
+ return false;
15
+ }
16
+ return true;
17
+ });
18
+ return empty;
19
+ }
20
+ /**
21
+ * Backspace/Delete 로 표를 정리한다. (main 정올 `plugin/table/deleteTable.ts` 참조 —
22
+ * 로직 재작성. 열·행 판정은 TableMap 순회 대신 `CellSelection` 의 자체 판정을 쓴다.)
23
+ *
24
+ * · 셀 선택(CellSelection)이 표 전체를 덮으면 → 표 삭제, 열 전체 → 열 삭제, 행 전체 → 행 삭제
25
+ * · 커서(TextSelection)가 **빈 표** 안에 있으면 → 표를 통째로 삭제
26
+ *
27
+ * 그 밖에는 전부 false — 셀 안의 글자 지우기 같은 기본 동작에 끼어들지 않는다.
28
+ */
29
+ const deleteTableOrSelection = ({ editor }) => {
30
+ if (!editor.isEditable)
31
+ return false;
32
+ const { selection } = editor.state;
33
+ if (selection instanceof CellSelection) {
34
+ const col = selection.isColSelection();
35
+ const row = selection.isRowSelection();
36
+ if (col && row)
37
+ return editor.commands.deleteTable();
38
+ if (col)
39
+ return editor.commands.deleteColumn();
40
+ if (row)
41
+ return editor.commands.deleteRow();
42
+ return false;
43
+ }
44
+ if (!selection.empty)
45
+ return false;
46
+ const { $from } = selection;
47
+ for (let d = $from.depth; d > 0; d--) {
48
+ const node = $from.node(d);
49
+ if (node.type.name !== "table")
50
+ continue;
51
+ if (!isTableEmpty(node))
52
+ return false;
53
+ return editor.commands.deleteTable();
54
+ }
55
+ return false;
56
+ };
57
+ /**
58
+ * 표 키보드 동작: `Tab` 다음 셀(마지막 셀이면 행을 만들어 이동), `Shift-Tab` 이전 셀,
59
+ * `Backspace`/`Delete` 로 빈 표·선택된 열/행/표 삭제. (main 정올
60
+ * `plugin/table/index.ts` 의 `addKeyboardShortcuts` 참조 — 로직 재작성.)
61
+ *
62
+ * ⚠️ **업스트림 `Table` 에도 같은 Tab 단축키가 있는데 왜 또 다는가** — TipTap 은 키맵
63
+ * 플러그인을 **등록 역순 + priority 내림차순**으로 배치한다(core 의
64
+ * `sortExtensions([...this.extensions].reverse())`). `TipTapEditor.svelte` 에서 `Indent` 가
65
+ * 표보다 **뒤에** 등록돼 있어, 기본 priority 로는 표 안에서도 Indent 의 Tab(NBSP 삽입)이
66
+ * 먼저 먹어 셀 이동이 죽는다. 그래서 여기만 priority 를 올려 표 안에서는 셀 이동이
67
+ * 이기게 한다. 표 밖에서는 모든 핸들러가 false 를 돌려주므로 Indent·리스트 들여쓰기는
68
+ * 그대로다.
69
+ */
70
+ export const TableKeymap = Extension.create({
71
+ name: "tableKeymap",
72
+ priority: 110,
73
+ addKeyboardShortcuts() {
74
+ return {
75
+ Tab: () => {
76
+ /* 읽기 전용에서도 keydown 은 들어온다 — 행 추가까지 가지 않게 막는다. */
77
+ if (!this.editor.isEditable)
78
+ return false;
79
+ if (this.editor.commands.goToNextCell())
80
+ return true;
81
+ /* 마지막 셀: 행을 하나 만들어 이어서 이동. 표 밖에서는 둘 다 실패해 false. */
82
+ if (!this.editor.can().addRowAfter())
83
+ return false;
84
+ return this.editor.chain().addRowAfter().goToNextCell().run();
85
+ },
86
+ "Shift-Tab": () => {
87
+ if (!this.editor.isEditable)
88
+ return false;
89
+ return this.editor.commands.goToPreviousCell();
90
+ },
91
+ Backspace: deleteTableOrSelection,
92
+ "Mod-Backspace": deleteTableOrSelection,
93
+ Delete: deleteTableOrSelection,
94
+ "Mod-Delete": deleteTableOrSelection
95
+ };
96
+ }
97
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=TableKeymap.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TableKeymap.test.d.ts","sourceRoot":"","sources":["../../src/extensions/TableKeymap.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { Editor } from "@tiptap/core";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import { Table } from "@tiptap/extension-table";
5
+ import { TableRow } from "@tiptap/extension-table-row";
6
+ import { TableHeader } from "@tiptap/extension-table-header";
7
+ import { TableCell } from "@tiptap/extension-table-cell";
8
+ import { CellSelection } from "@tiptap/pm/tables";
9
+ import { TableKeymap } from "./TableKeymap";
10
+ import { Indent } from "./Indent";
11
+ const NBSP = "\u00a0";
12
+ describe("TableKeymap", () => {
13
+ let editor;
14
+ /*
15
+ * 등록 순서를 `TipTapEditor.svelte` 와 같게 둔다 — 표 계열 뒤에 `Indent`.
16
+ * TipTap 키맵은 등록 역순이라 기본 priority 면 Indent 의 Tab 이 표를 가린다.
17
+ * 이 테스트 절반은 그 순서 문제(priority 110)가 무너지지 않는지 잡는 것이다.
18
+ */
19
+ function createEditor(content) {
20
+ editor = new Editor({
21
+ element: document.createElement("div"),
22
+ extensions: [StarterKit, Table, TableRow, TableHeader, TableCell, TableKeymap, Indent],
23
+ content
24
+ });
25
+ return editor;
26
+ }
27
+ afterEach(() => {
28
+ editor?.destroy();
29
+ });
30
+ function press(key, init = {}) {
31
+ const event = new KeyboardEvent("keydown", { key, ...init });
32
+ return editor.view.someProp("handleKeyDown", (f) => f(editor.view, event)) ?? false;
33
+ }
34
+ /** 본문 텍스트 `text` 가 시작하는 문서 위치. */
35
+ function posOf(text) {
36
+ let pos = -1;
37
+ editor.state.doc.descendants((node, p) => {
38
+ if (pos !== -1)
39
+ return false;
40
+ if (node.isText && node.text?.includes(text))
41
+ pos = p;
42
+ return pos === -1;
43
+ });
44
+ if (pos === -1)
45
+ throw new Error(`"${text}" not found`);
46
+ return pos;
47
+ }
48
+ /** 첫 셀(tableCell/tableHeader) 노드의 문서 위치. */
49
+ function cellPositions() {
50
+ const cells = [];
51
+ editor.state.doc.descendants((node, p) => {
52
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader")
53
+ cells.push(p);
54
+ return true;
55
+ });
56
+ return cells;
57
+ }
58
+ function hasTable() {
59
+ let found = false;
60
+ editor.state.doc.descendants((node) => {
61
+ if (node.type.name === "table")
62
+ found = true;
63
+ return !found;
64
+ });
65
+ return found;
66
+ }
67
+ function rowCount() {
68
+ let rows = 0;
69
+ editor.state.doc.descendants((node) => {
70
+ if (node.type.name === "tableRow")
71
+ rows += 1;
72
+ return true;
73
+ });
74
+ return rows;
75
+ }
76
+ const TWO_CELLS = "<table><tbody><tr><td><p>a</p></td><td><p>b</p></td></tr></tbody></table>";
77
+ const EMPTY_TABLE = "<table><tbody><tr><td><p></p></td><td><p></p></td></tr></tbody></table><p>after</p>";
78
+ it("Tab 은 다음 셀로 이동한다 — Indent 의 NBSP 삽입이 가로채지 않는다", () => {
79
+ createEditor(TWO_CELLS);
80
+ editor.commands.setTextSelection(posOf("a") + 1);
81
+ expect(press("Tab")).toBe(true);
82
+ expect(editor.state.selection.$from.parent.textContent).toBe("b");
83
+ expect(editor.getText()).not.toContain(NBSP);
84
+ });
85
+ it("마지막 셀에서 Tab 은 행을 만들어 이동한다", () => {
86
+ createEditor(TWO_CELLS);
87
+ editor.commands.setTextSelection(posOf("b") + 1);
88
+ expect(rowCount()).toBe(1);
89
+ expect(press("Tab")).toBe(true);
90
+ expect(rowCount()).toBe(2);
91
+ /* 새 행의 첫 셀(빈 문단)에 있어야 한다. */
92
+ expect(editor.state.selection.$from.parent.textContent).toBe("");
93
+ });
94
+ it("Shift-Tab 은 이전 셀로 이동한다", () => {
95
+ createEditor(TWO_CELLS);
96
+ editor.commands.setTextSelection(posOf("b") + 1);
97
+ expect(press("Tab", { shiftKey: true })).toBe(true);
98
+ expect(editor.state.selection.$from.parent.textContent).toBe("a");
99
+ });
100
+ it("표 밖 Tab 은 손대지 않는다 — Indent 가 그대로 동작한다", () => {
101
+ createEditor("<p>outside</p>");
102
+ editor.commands.setTextSelection(posOf("outside") + 1);
103
+ expect(press("Tab")).toBe(true);
104
+ expect(editor.getText()).toContain(NBSP.repeat(4));
105
+ });
106
+ it("빈 표 안에서 Backspace 는 표를 통째로 지운다", () => {
107
+ createEditor(EMPTY_TABLE);
108
+ /* 첫 셀의 빈 문단 안(셀 위치 +2)에 커서를 둔다. */
109
+ editor.commands.setTextSelection(cellPositions()[0] + 2);
110
+ expect(press("Backspace")).toBe(true);
111
+ expect(hasTable()).toBe(false);
112
+ expect(editor.getText()).toContain("after");
113
+ });
114
+ it("빈 표 안에서 Delete 도 표를 지운다", () => {
115
+ createEditor(EMPTY_TABLE);
116
+ editor.commands.setTextSelection(cellPositions()[0] + 2);
117
+ expect(press("Delete")).toBe(true);
118
+ expect(hasTable()).toBe(false);
119
+ });
120
+ it("내용이 있는 표에서는 Backspace 로 표를 지우지 않는다", () => {
121
+ createEditor(TWO_CELLS);
122
+ editor.commands.setTextSelection(posOf("a"));
123
+ press("Backspace");
124
+ expect(hasTable()).toBe(true);
125
+ expect(editor.getText()).toContain("a");
126
+ });
127
+ it("표 전체 셀 선택 + Backspace 는 표를 지운다", () => {
128
+ createEditor(TWO_CELLS);
129
+ const cells = cellPositions();
130
+ const sel = CellSelection.create(editor.state.doc, cells[0], cells[cells.length - 1]);
131
+ editor.view.dispatch(editor.state.tr.setSelection(sel));
132
+ expect(press("Backspace")).toBe(true);
133
+ expect(hasTable()).toBe(false);
134
+ });
135
+ it("열 전체 선택 + Backspace 는 그 열만 지운다", () => {
136
+ createEditor("<table><tbody>" +
137
+ "<tr><td><p>a</p></td><td><p>b</p></td></tr>" +
138
+ "<tr><td><p>c</p></td><td><p>d</p></td></tr>" +
139
+ "</tbody></table>");
140
+ const sel = CellSelection.colSelection(editor.state.doc.resolve(cellPositions()[0]));
141
+ editor.view.dispatch(editor.state.tr.setSelection(sel));
142
+ expect(press("Backspace")).toBe(true);
143
+ expect(hasTable()).toBe(true);
144
+ expect(editor.getText()).not.toContain("a");
145
+ expect(editor.getText()).toContain("b");
146
+ });
147
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"TabsBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/TabsBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AA4DrD;;;;;GAKG;AACH,eAAO,MAAM,GAAG,gBAqDd,CAAC;AAEH,eAAO,MAAM,SAAS,gBAinBpB,CAAC;AAEH,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,IAAI,EAAE;YACL,qCAAqC;YACrC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;SACxC,CAAC;KACF;CACD"}
1
+ {"version":3,"file":"TabsBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/TabsBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AA6DrD;;;;;GAKG;AACH,eAAO,MAAM,GAAG,gBAqDd,CAAC;AAEH,eAAO,MAAM,SAAS,gBAmnBpB,CAAC;AAEH,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,IAAI,EAAE;YACL,qCAAqC;YACrC,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;SACxC,CAAC;KACF;CACD"}
@@ -1,4 +1,5 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { getEditorTranslator } from "../i18n";
2
3
  import { Selection } from "@tiptap/pm/state";
3
4
  /**
4
5
  * 탭 블록 — 한 상자 안의 내용을 **탭으로 나눠** 한 번에 하나만 보여준다(노션의 그것).
@@ -33,7 +34,7 @@ import { Selection } from "@tiptap/pm/state";
33
34
  * (`editor.css` 의 `:not(.hce-tabs-live)` 규칙). 그래야 에디터가 서기 전후로 높이가 튀지 않는다.
34
35
  */
35
36
  /** 이름을 따로 주지 않은 탭이 보여줄 기본 이름. */
36
- const defaultTitle = (index) => `탭 ${index + 1}`;
37
+ const defaultTitle = (t, index) => t("tabDefaultTitle", { n: index + 1 });
37
38
  /**
38
39
  * 여기까지는 **클릭이다.** 손가락·마우스는 누를 때 몇 px 씩 흔들리므로, 이만큼 넘어야
39
40
  * "끌었다"로 본다. 넘지 않고 떼면 평소대로 탭 전환이 되고 두 번 누르면 이름 고치기가 뜬다.
@@ -120,6 +121,7 @@ export const TabsBlock = Node.create({
120
121
  },
121
122
  addNodeView() {
122
123
  return ({ node, editor, getPos }) => {
124
+ const t = getEditorTranslator(editor);
123
125
  let currentNode = node;
124
126
  /** 지금 보고 있는 탭. 문서에 저장하지 않는다(파일 첫머리 주석 참고). */
125
127
  let active = 0;
@@ -170,7 +172,7 @@ export const TabsBlock = Node.create({
170
172
  };
171
173
  const titleOf = (index) => {
172
174
  const raw = String(currentNode.child(index).attrs.title ?? "").trim();
173
- return raw || defaultTitle(index);
175
+ return raw || defaultTitle(t, index);
174
176
  };
175
177
  /** `index` 번째 탭 노드의 문서상 위치. `tabs` 가 문서에서 빠졌으면 null. */
176
178
  const childPos = (index) => {
@@ -287,7 +289,7 @@ export const TabsBlock = Node.create({
287
289
  return;
288
290
  const index = currentNode.childCount;
289
291
  // `content: "block+"` 이라 `createAndFill` 이 빈 문단 하나를 채워 준다.
290
- const created = type.createAndFill({ title: defaultTitle(index) });
292
+ const created = type.createAndFill({ title: defaultTitle(t, index) });
291
293
  if (!created)
292
294
  return;
293
295
  active = index;
@@ -557,7 +559,7 @@ export const TabsBlock = Node.create({
557
559
  setActive(index);
558
560
  });
559
561
  if (editable) {
560
- label.title = "두 번 누르면 이름을 고칩니다";
562
+ label.title = t("tabRenameTip");
561
563
  label.addEventListener("dblclick", (e) => {
562
564
  e.preventDefault();
563
565
  e.stopPropagation();
@@ -581,7 +583,7 @@ export const TabsBlock = Node.create({
581
583
  close.className = deletable
582
584
  ? "hce-tab-chip-close"
583
585
  : "hce-tab-chip-close is-disabled";
584
- close.setAttribute("aria-label", `${titleOf(index)} 삭제`);
586
+ close.setAttribute("aria-label", t("tabDelete", { title: titleOf(index) }));
585
587
  close.textContent = "×";
586
588
  if (deletable) {
587
589
  close.addEventListener("mousedown", (e) => e.preventDefault());
@@ -604,8 +606,8 @@ export const TabsBlock = Node.create({
604
606
  const add = document.createElement("button");
605
607
  add.type = "button";
606
608
  add.className = "hce-tabs-add";
607
- add.setAttribute("aria-label", "탭 추가");
608
- add.title = "탭 추가";
609
+ add.setAttribute("aria-label", t("tabAdd"));
610
+ add.title = t("tabAdd");
609
611
  add.textContent = "+";
610
612
  add.addEventListener("mousedown", (e) => e.preventDefault());
611
613
  add.addEventListener("click", (e) => {
@@ -669,14 +671,15 @@ export const TabsBlock = Node.create({
669
671
  },
670
672
  addCommands() {
671
673
  return {
672
- setTabs: (count = 3) => ({ chain }) => {
674
+ setTabs: (count = 3) => ({ chain, editor }) => {
675
+ const t = getEditorTranslator(editor);
673
676
  const total = Math.max(1, Math.floor(count));
674
677
  return (chain()
675
678
  .insertContent({
676
679
  type: this.name,
677
680
  content: Array.from({ length: total }, (_unused, i) => ({
678
681
  type: "tab",
679
- attrs: { title: defaultTitle(i) },
682
+ attrs: { title: defaultTitle(t, i) },
680
683
  content: [{ type: "paragraph" }]
681
684
  }))
682
685
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TiptapMidibus.d.ts","sourceRoot":"","sources":["../../src/extensions/TiptapMidibus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,IAAI,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhE,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACpC,oCAAoC;IACpC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,mBAAmB,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CACjC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,aAAa,EAAE;YACd,gBAAgB,EAAE,CAAC,KAAK,EAAE;gBACzB,EAAE,EAAE,MAAM,CAAC;gBACX,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;gBACxB,IAAI,CAAC,EAAE,MAAM,CAAC;gBACd,KAAK,CAAC,EAAE,MAAM,CAAC;gBACf,MAAM,CAAC,EAAE,MAAM,CAAC;aAChB,KAAK,UAAU,CAAC;SACjB,CAAC;KACF;CACD;AAED,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAiBrD,eAAO,MAAM,aAAa,iCAgJxB,CAAC"}
1
+ {"version":3,"file":"TiptapMidibus.d.ts","sourceRoot":"","sources":["../../src/extensions/TiptapMidibus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,IAAI,EAAmB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAKhE,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACpC,oCAAoC;IACpC,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,mBAAmB,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CACjC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,aAAa,EAAE;YACd,gBAAgB,EAAE,CAAC,KAAK,EAAE;gBACzB,EAAE,EAAE,MAAM,CAAC;gBACX,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;gBACxB,IAAI,CAAC,EAAE,MAAM,CAAC;gBACd,KAAK,CAAC,EAAE,MAAM,CAAC;gBACf,MAAM,CAAC,EAAE,MAAM,CAAC;aAChB,KAAK,UAAU,CAAC;SACjB,CAAC;KACF;CACD;AAED,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAiBrD,eAAO,MAAM,aAAa,iCAoMxB,CAAC"}