@teriusu/rich-editor 0.9.5 → 0.9.7

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 (53) hide show
  1. package/dist/components/BubbleToolbar.svelte +102 -49
  2. package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
  3. package/dist/components/FixedToolbar.svelte +136 -214
  4. package/dist/components/FixedToolbar.svelte.d.ts +9 -1
  5. package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
  6. package/dist/components/InputModal.svelte +1 -1
  7. package/dist/components/MathModal.svelte +111 -1
  8. package/dist/components/MathModal.svelte.d.ts.map +1 -1
  9. package/dist/components/MediaPickerModal.svelte +206 -0
  10. package/dist/components/MediaPickerModal.svelte.d.ts +18 -0
  11. package/dist/components/MediaPickerModal.svelte.d.ts.map +1 -0
  12. package/dist/components/SlashCommandMenu.svelte +114 -13
  13. package/dist/components/SlashCommandMenu.svelte.d.ts +10 -1
  14. package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
  15. package/dist/components/TableBubbleMenu.svelte +71 -48
  16. package/dist/components/TableBubbleMenu.svelte.d.ts.map +1 -1
  17. package/dist/components/TipTapEditor.svelte +180 -24
  18. package/dist/components/TipTapEditor.svelte.d.ts +2 -0
  19. package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
  20. package/dist/extensions/NotionInputRules.d.ts +59 -0
  21. package/dist/extensions/NotionInputRules.d.ts.map +1 -0
  22. package/dist/extensions/NotionInputRules.js +141 -0
  23. package/dist/extensions/NotionInputRules.test.d.ts +2 -0
  24. package/dist/extensions/NotionInputRules.test.d.ts.map +1 -0
  25. package/dist/extensions/NotionInputRules.test.js +117 -0
  26. package/dist/extensions/VideoEmbed.d.ts +36 -0
  27. package/dist/extensions/VideoEmbed.d.ts.map +1 -0
  28. package/dist/extensions/VideoEmbed.js +246 -0
  29. package/dist/extensions/VideoEmbed.test.d.ts +2 -0
  30. package/dist/extensions/VideoEmbed.test.d.ts.map +1 -0
  31. package/dist/extensions/VideoEmbed.test.js +57 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +13 -0
  35. package/dist/styles/editor.css +1 -1
  36. package/dist/types.d.ts +14 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +20 -6
  39. package/dist/utils/lowlight.d.ts +17 -0
  40. package/dist/utils/lowlight.d.ts.map +1 -0
  41. package/dist/utils/lowlight.js +106 -0
  42. package/dist/utils/lowlight.test.d.ts +2 -0
  43. package/dist/utils/lowlight.test.d.ts.map +1 -0
  44. package/dist/utils/lowlight.test.js +56 -0
  45. package/dist/utils/sanitize.d.ts.map +1 -1
  46. package/dist/utils/sanitize.js +14 -0
  47. package/dist/utils/splitCell.d.ts +20 -0
  48. package/dist/utils/splitCell.d.ts.map +1 -0
  49. package/dist/utils/splitCell.js +74 -0
  50. package/dist/utils/table.d.ts +22 -0
  51. package/dist/utils/table.d.ts.map +1 -0
  52. package/dist/utils/table.js +68 -0
  53. package/package.json +6 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAwB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AAqXJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,CAAC;AA0bJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -18,6 +18,7 @@
18
18
  } from "lucide-svelte";
19
19
  import { onMount } from "svelte";
20
20
  import { cn } from "../utils/cn";
21
+ import { splitCellPreservingHeaders } from "../utils/splitCell";
21
22
 
22
23
  const PRESET_COLORS = [
23
24
  { label: "없음", value: "" },
@@ -94,11 +95,29 @@
94
95
  visible = true;
95
96
  }
96
97
 
98
+ /*
99
+ * ⚠️ **버튼의 활성 여부가 갱신되게 하는 장치.**
100
+ *
101
+ * `editor` 는 TipTap 이 만든 평범한 객체라 `$state` 가 아니다. 그래서 `editor.can()` 을
102
+ * 그냥 부르면 Svelte 가 의존성을 못 보고 **처음 값에 굳는다** — 셀을 여러 개 골라도
103
+ * `mergeCells()` 가능 여부가 다시 계산되지 않아 **`셀 병합`·`셀 분할` 이 영영 비활성**
104
+ * 이었다(사용자 지적: "표에서 셀병합 분할이 안되네?").
105
+ *
106
+ * 고정 툴바가 같은 이유로 이미 쓰고 있는 방식이다 — 트랜잭션마다 카운터를 올리고
107
+ * 활성 여부를 읽는 자리에서 그 카운터를 함께 읽는다(쉼표 연산자).
108
+ */
109
+ let tick = $state(0);
110
+ const canDo = (fn: (c: ReturnType<Editor["can"]>) => boolean) =>
111
+ (tick, fn(editor.can()));
112
+
97
113
  onMount(() => {
114
+ const bump = () => tick++;
115
+ editor.on("transaction", bump);
98
116
  editor.on("selectionUpdate", updatePosition);
99
117
  editor.on("update", updatePosition);
100
118
 
101
119
  return () => {
120
+ editor.off("transaction", bump);
102
121
  editor.off("selectionUpdate", updatePosition);
103
122
  editor.off("update", updatePosition);
104
123
  };
@@ -121,21 +140,25 @@
121
140
  <div
122
141
  bind:this={menuEl}
123
142
  class="absolute hce-table-bubble"
143
+ onmousedown={(e) => e.preventDefault()}
144
+ role="toolbar"
145
+ tabindex="-1"
146
+ aria-label="표 편집"
124
147
  style="top: {pos.top}px; left: {pos.left}px; width: {pos.width}px"
125
148
  >
126
149
  <div
127
- class="flex items-center justify-center gap-0.5 px-1.5 py-1 hce-menu-surface rounded-full shadow-xl w-fit mx-auto"
150
+ class="flex items-center justify-center gap-0.5 px-1.5 py-1 bg-popover border border-border rounded-full shadow-xl w-fit mx-auto"
128
151
  >
129
152
  <!-- Row/Col buttons -->
130
153
  <div class="hce-table-btn-wrap">
131
154
  <button
132
155
  type="button"
133
156
  onclick={() => editor.chain().focus().addRowBefore().run()}
134
- disabled={!editor.can().addRowBefore()}
157
+ disabled={!canDo((c) => c.addRowBefore())}
135
158
  class={cn(
136
159
  "p-1.5 rounded-full transition-colors",
137
- "text-white/70 hover:text-white hover:bg-white/10",
138
- !editor.can().addRowBefore() && "opacity-30 pointer-events-none",
160
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
161
+ !canDo((c) => c.addRowBefore()) && "opacity-30 pointer-events-none",
139
162
  )}
140
163
  >
141
164
  <ArrowUpToLine size={iconSize} />
@@ -147,11 +170,11 @@
147
170
  <button
148
171
  type="button"
149
172
  onclick={() => editor.chain().focus().addRowAfter().run()}
150
- disabled={!editor.can().addRowAfter()}
173
+ disabled={!canDo((c) => c.addRowAfter())}
151
174
  class={cn(
152
175
  "p-1.5 rounded-full transition-colors",
153
- "text-white/70 hover:text-white hover:bg-white/10",
154
- !editor.can().addRowAfter() && "opacity-30 pointer-events-none",
176
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
177
+ !canDo((c) => c.addRowAfter()) && "opacity-30 pointer-events-none",
155
178
  )}
156
179
  >
157
180
  <ArrowDownToLine size={iconSize} />
@@ -163,11 +186,11 @@
163
186
  <button
164
187
  type="button"
165
188
  onclick={() => editor.chain().focus().addColumnBefore().run()}
166
- disabled={!editor.can().addColumnBefore()}
189
+ disabled={!canDo((c) => c.addColumnBefore())}
167
190
  class={cn(
168
191
  "p-1.5 rounded-full transition-colors",
169
- "text-white/70 hover:text-white hover:bg-white/10",
170
- !editor.can().addColumnBefore() &&
192
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
193
+ !canDo((c) => c.addColumnBefore()) &&
171
194
  "opacity-30 pointer-events-none",
172
195
  )}
173
196
  >
@@ -180,11 +203,11 @@
180
203
  <button
181
204
  type="button"
182
205
  onclick={() => editor.chain().focus().addColumnAfter().run()}
183
- disabled={!editor.can().addColumnAfter()}
206
+ disabled={!canDo((c) => c.addColumnAfter())}
184
207
  class={cn(
185
208
  "p-1.5 rounded-full transition-colors",
186
- "text-white/70 hover:text-white hover:bg-white/10",
187
- !editor.can().addColumnAfter() &&
209
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
210
+ !canDo((c) => c.addColumnAfter()) &&
188
211
  "opacity-30 pointer-events-none",
189
212
  )}
190
213
  >
@@ -193,17 +216,17 @@
193
216
  <span class="hce-table-btn-tooltip">열 오른쪽에 추가</span>
194
217
  </div>
195
218
 
196
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
219
+ <div class="w-px h-5 bg-border mx-0.5"></div>
197
220
 
198
221
  <div class="hce-table-btn-wrap">
199
222
  <button
200
223
  type="button"
201
224
  onclick={() => editor.chain().focus().toggleHeaderRow().run()}
202
- disabled={!editor.can().toggleHeaderRow()}
225
+ disabled={!canDo((c) => c.toggleHeaderRow())}
203
226
  class={cn(
204
227
  "p-1.5 rounded-full transition-colors",
205
- "text-white/70 hover:text-white hover:bg-white/10",
206
- !editor.can().toggleHeaderRow() &&
228
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
229
+ !canDo((c) => c.toggleHeaderRow()) &&
207
230
  "opacity-30 pointer-events-none",
208
231
  )}
209
232
  >
@@ -216,11 +239,11 @@
216
239
  <button
217
240
  type="button"
218
241
  onclick={() => editor.chain().focus().toggleHeaderColumn().run()}
219
- disabled={!editor.can().toggleHeaderColumn()}
242
+ disabled={!canDo((c) => c.toggleHeaderColumn())}
220
243
  class={cn(
221
244
  "p-1.5 rounded-full transition-colors",
222
- "text-white/70 hover:text-white hover:bg-white/10",
223
- !editor.can().toggleHeaderColumn() &&
245
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
246
+ !canDo((c) => c.toggleHeaderColumn()) &&
224
247
  "opacity-30 pointer-events-none",
225
248
  )}
226
249
  >
@@ -233,11 +256,11 @@
233
256
  <button
234
257
  type="button"
235
258
  onclick={() => editor.chain().focus().mergeCells().run()}
236
- disabled={!editor.can().mergeCells()}
259
+ disabled={!canDo((c) => c.mergeCells())}
237
260
  class={cn(
238
261
  "p-1.5 rounded-full transition-colors",
239
- "text-white/70 hover:text-white hover:bg-white/10",
240
- !editor.can().mergeCells() && "opacity-30 pointer-events-none",
262
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
263
+ !canDo((c) => c.mergeCells()) && "opacity-30 pointer-events-none",
241
264
  )}
242
265
  >
243
266
  <Combine size={iconSize} />
@@ -248,12 +271,12 @@
248
271
  <div class="hce-table-btn-wrap">
249
272
  <button
250
273
  type="button"
251
- onclick={() => editor.chain().focus().splitCell().run()}
252
- disabled={!editor.can().splitCell()}
274
+ onclick={() => splitCellPreservingHeaders(editor)}
275
+ disabled={!canDo((c) => c.splitCell())}
253
276
  class={cn(
254
277
  "p-1.5 rounded-full transition-colors",
255
- "text-white/70 hover:text-white hover:bg-white/10",
256
- !editor.can().splitCell() && "opacity-30 pointer-events-none",
278
+ "text-muted-foreground hover:text-foreground hover:bg-muted",
279
+ !canDo((c) => c.splitCell()) && "opacity-30 pointer-events-none",
257
280
  )}
258
281
  >
259
282
  <SplitSquareHorizontal size={iconSize} />
@@ -261,18 +284,18 @@
261
284
  <span class="hce-table-btn-tooltip">셀 분할</span>
262
285
  </div>
263
286
 
264
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
287
+ <div class="w-px h-5 bg-border mx-0.5"></div>
265
288
 
266
289
  <!-- Line height -->
267
290
  <div class="relative">
268
- <div class="hce-table-btn-wrap">
291
+ <div class="hce-table-btn-wrap" class:hce-tooltip-off={showLineHeight}>
269
292
  <button
270
293
  type="button"
271
294
  onclick={() => {
272
295
  showLineHeight = !showLineHeight;
273
296
  showColors = false;
274
297
  }}
275
- class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
298
+ class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
276
299
  >
277
300
  <UnfoldVertical size={iconSize} />
278
301
  </button>
@@ -280,13 +303,13 @@
280
303
  </div>
281
304
  {#if showLineHeight}
282
305
  <div
283
- class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hce-menu-surface rounded-lg shadow-xl border border-white/10 py-1"
306
+ class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-popover border border-border rounded-lg shadow-xl py-1"
284
307
  style="min-width: 100px"
285
308
  >
286
309
  {#each LINE_HEIGHTS as lh}
287
310
  <button
288
311
  type="button"
289
- class="w-full text-left px-3 py-1.5 text-xs text-white/80 hover:text-white hover:bg-white/10 transition-colors"
312
+ class="w-full text-left px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
290
313
  onclick={() => {
291
314
  const { tr } = editor.state;
292
315
  const { tableNode, tablePos } = findTableNode();
@@ -325,7 +348,7 @@
325
348
  <div class="hce-table-btn-wrap">
326
349
  <button
327
350
  type="button"
328
- class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
351
+ class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
329
352
  onclick={() => {
330
353
  const { tableNode, tablePos } = findTableNode();
331
354
  if (!tableNode || tablePos < 0) return;
@@ -369,14 +392,14 @@
369
392
 
370
393
  <!-- Background color -->
371
394
  <div class="relative">
372
- <div class="hce-table-btn-wrap">
395
+ <div class="hce-table-btn-wrap" class:hce-tooltip-off={showColors}>
373
396
  <button
374
397
  type="button"
375
398
  onclick={() => {
376
399
  showColors = !showColors;
377
400
  showLineHeight = false;
378
401
  }}
379
- class="p-1.5 rounded-full transition-colors text-white/70 hover:text-white hover:bg-white/10"
402
+ class="p-1.5 rounded-full transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
380
403
  >
381
404
  <Paintbrush size={iconSize} />
382
405
  </button>
@@ -384,11 +407,11 @@
384
407
  </div>
385
408
  {#if showColors}
386
409
  <div
387
- class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hce-menu-surface rounded-lg shadow-xl border border-white/10 p-2"
410
+ class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-popover border border-border rounded-lg shadow-xl p-2"
388
411
  style="min-width: 160px"
389
412
  >
390
413
  <p
391
- class="text-xs font-medium text-white/60 mb-1.5 px-1"
414
+ class="text-xs font-medium text-muted-foreground mb-1.5 px-1"
392
415
  >
393
416
  배경색
394
417
  </p>
@@ -397,7 +420,7 @@
397
420
  <button
398
421
  type="button"
399
422
  title={c.label}
400
- class="w-full h-7 rounded-md border border-white/20 transition-transform hover:scale-110"
423
+ class="w-full h-7 rounded-md border border-border transition-transform hover:scale-110"
401
424
  style="background: {c.value ||
402
425
  'transparent'}; {!c.value
403
426
  ? 'background-image: linear-gradient(135deg, transparent 45%, #ef4444 45%, #ef4444 55%, transparent 55%)'
@@ -417,18 +440,18 @@
417
440
  {/if}
418
441
  </div>
419
442
 
420
- <div class="w-px h-5 bg-white/20 mx-0.5"></div>
443
+ <div class="w-px h-5 bg-border mx-0.5"></div>
421
444
 
422
445
  <!-- Delete buttons -->
423
446
  <div class="hce-table-btn-wrap">
424
447
  <button
425
448
  type="button"
426
449
  onclick={() => editor.chain().focus().deleteRow().run()}
427
- disabled={!editor.can().deleteRow()}
450
+ disabled={!canDo((c) => c.deleteRow())}
428
451
  class={cn(
429
452
  "p-1.5 rounded-full transition-colors",
430
- "text-white/70 hover:text-red-300 hover:bg-white/10",
431
- !editor.can().deleteRow() && "opacity-30 pointer-events-none",
453
+ "text-muted-foreground hover:text-red-300 hover:bg-muted",
454
+ !canDo((c) => c.deleteRow()) && "opacity-30 pointer-events-none",
432
455
  )}
433
456
  >
434
457
  <RowsIcon size={iconSize} />
@@ -440,11 +463,11 @@
440
463
  <button
441
464
  type="button"
442
465
  onclick={() => editor.chain().focus().deleteColumn().run()}
443
- disabled={!editor.can().deleteColumn()}
466
+ disabled={!canDo((c) => c.deleteColumn())}
444
467
  class={cn(
445
468
  "p-1.5 rounded-full transition-colors",
446
- "text-white/70 hover:text-red-300 hover:bg-white/10",
447
- !editor.can().deleteColumn() && "opacity-30 pointer-events-none",
469
+ "text-muted-foreground hover:text-red-300 hover:bg-muted",
470
+ !canDo((c) => c.deleteColumn()) && "opacity-30 pointer-events-none",
448
471
  )}
449
472
  >
450
473
  <ColumnsIcon size={iconSize} />
@@ -456,11 +479,11 @@
456
479
  <button
457
480
  type="button"
458
481
  onclick={() => editor.chain().focus().deleteTable().run()}
459
- disabled={!editor.can().deleteTable()}
482
+ disabled={!canDo((c) => c.deleteTable())}
460
483
  class={cn(
461
484
  "p-1.5 rounded-full transition-colors",
462
- "text-white/70 hover:text-red-300 hover:bg-white/10",
463
- !editor.can().deleteTable() && "opacity-30 pointer-events-none",
485
+ "text-muted-foreground hover:text-red-300 hover:bg-muted",
486
+ !canDo((c) => c.deleteTable()) && "opacity-30 pointer-events-none",
464
487
  )}
465
488
  >
466
489
  <Trash2 size={iconSize} />
@@ -1 +1 @@
1
- {"version":3,"file":"TableBubbleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TableBubbleMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAoB1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAkX7C,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"TableBubbleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TableBubbleMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAqY7C,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -4,24 +4,7 @@
4
4
  import { TableCell } from "@tiptap/extension-table-cell";
5
5
  import { Extension } from "@tiptap/core";
6
6
  import { TextSelection } from "@tiptap/pm/state";
7
- import { common, createLowlight } from "lowlight";
8
- import cpp from "highlight.js/lib/languages/cpp";
9
- import python from "highlight.js/lib/languages/python";
10
-
11
- /*
12
- * 문법 하이라이터는 **모듈에 한 벌만** 둔다.
13
- *
14
- * `common` 은 언어 37종이고, 예전엔 이 등록이 인스턴스 스크립트에 있어 에디터를 세울
15
- * 때마다 통째로 다시 돌았다. 정올 기출 퀴즈처럼 한 화면에 에디터가 26~27개 서는
16
- * 곳에서는 그게 1000번 가까이 반복된다.
17
- *
18
- * lowlight 는 문법을 담아 두는 레지스트리일 뿐 편집기별 상태를 갖지 않으므로 공유해도
19
- * 안전하다. (측정해 보면 이 항목 자체는 27개 기준 13ms 수준이라 큰 몫은 아니다 —
20
- * 다만 공짜로 없앨 수 있는 반복이라 남겨 둘 이유가 없다.)
21
- */
22
- const lowlight = createLowlight(common);
23
- lowlight.register("cpp", cpp);
24
- lowlight.register("python", python);
7
+ import { lowlight } from "../utils/lowlight";
25
8
 
26
9
  /*
27
10
  * 읽기 전용용 표 확장도 **한 벌만** 만든다.
@@ -128,8 +111,13 @@
128
111
  import Typography from "@tiptap/extension-typography";
129
112
  import CharacterCount from "@tiptap/extension-character-count";
130
113
  import { TableRow } from "@tiptap/extension-table-row";
131
- import { DetailsContent, DetailsSummary } from "@tiptap/extension-details";
114
+ import { DetailsContent } from "@tiptap/extension-details";
132
115
  import { FixedDetails } from "../extensions/FixedDetails";
116
+ import {
117
+ NotionBlockquote,
118
+ LeveledDetailsSummary,
119
+ NotionToggleInputRule,
120
+ } from "../extensions/NotionInputRules";
133
121
  import FileHandler from "@tiptap/extension-file-handler";
134
122
  import type { AnyExtension } from "@tiptap/core";
135
123
  import { PdfBlock } from "../extensions/PdfBlock";
@@ -139,6 +127,7 @@
139
127
  import { Indent } from "../extensions/Indent";
140
128
  import { FileAttachment } from "../extensions/FileAttachment";
141
129
  import { MbusVideo } from "../extensions/MbusVideo";
130
+ import { VideoEmbed } from "../extensions/VideoEmbed";
142
131
  import { CardBlock } from "../extensions/CardBlock";
143
132
  import { MathInline, MathDisplay, type MathPrompt } from "../extensions/Math";
144
133
  import FixedToolbar from "./FixedToolbar.svelte";
@@ -146,6 +135,8 @@
146
135
  import SlashCommandMenu from "./SlashCommandMenu.svelte";
147
136
  import TableBubbleMenu from "./TableBubbleMenu.svelte";
148
137
  import MathModal from "./MathModal.svelte";
138
+ import MediaPickerModal from "./MediaPickerModal.svelte";
139
+ import InputModal from "./InputModal.svelte";
149
140
  import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
150
141
  import { resolveFeatures } from "../types";
151
142
  import type { FileResolver } from "../extensions/FileAttachment";
@@ -160,6 +151,7 @@
160
151
  onPromptLink,
161
152
  onPromptImage,
162
153
  onPromptMbus,
154
+ onPromptVideo,
163
155
  onPromptCardBackground,
164
156
  onPromptMath,
165
157
  toolbarEnd,
@@ -177,6 +169,8 @@
177
169
  onPromptLink?: PromptHandler;
178
170
  onPromptImage?: PromptHandler;
179
171
  onPromptMbus?: PromptHandler;
172
+ /** 영상(유튜브·Vimeo 등) URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
173
+ onPromptVideo?: PromptHandler;
180
174
  /** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
181
175
  onPromptCardBackground?: PromptHandler;
182
176
  /** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
@@ -238,6 +232,93 @@
238
232
  let uploading = $state(false);
239
233
  let pdfInputEl: HTMLInputElement | undefined = $state();
240
234
  let fileInputEl: HTMLInputElement | undefined = $state();
235
+
236
+ /*
237
+ * ── 이미지·파일 고르기 모달 ──────────────────────────────────────────────
238
+ * **에디터가 갖는다.** 툴바에서도 슬래시 메뉴에서도 같은 것이 열려야 하고, 실제로 넣는 데
239
+ * 필요한 것(`onUploadFile`, 노드 만드는 법)이 전부 여기 있기 때문이다. 툴바에 두면
240
+ * 슬래시 메뉴가 자기 것을 또 만들어야 한다.
241
+ *
242
+ * ⚠️ **업로드 함수를 준 호스트에게만 연다.** 못 올리는 곳에서 `업로드` 탭을 보여 주면
243
+ * 눌러도 아무 일이 없다. 그런 곳은 예전처럼 URL 입력만 받는다(아래 `pickImage`).
244
+ */
245
+ let mediaPicker = $state<"image" | "file" | null>(null);
246
+
247
+ /** URL 에서 파일 이름을 뽑는다. 링크로 첨부할 때 표시할 이름이 필요하다. */
248
+ function fileNameFromUrl(raw: string): string {
249
+ try {
250
+ const path = new URL(raw, "http://x").pathname;
251
+ const last = decodeURIComponent(path.split("/").filter(Boolean).pop() || "");
252
+ return last || "파일";
253
+ } catch {
254
+ return "파일";
255
+ }
256
+ }
257
+
258
+ function pickImage() {
259
+ if (onUploadFile) {
260
+ mediaPicker = "image";
261
+ return;
262
+ }
263
+ // 못 올리는 호스트 — 예전 경로(호스트 프롬프트 → 없으면 내장 입력창) 그대로.
264
+ if (onPromptImage) {
265
+ onPromptImage("").then((url) => {
266
+ if (url) editor?.chain().focus().setImage({ src: url }).run();
267
+ });
268
+ return;
269
+ }
270
+ imageUrlPrompt = true;
271
+ }
272
+
273
+ function pickFile() {
274
+ if (!onUploadFile) return;
275
+ mediaPicker = "file";
276
+ }
277
+
278
+ /** 업로드 없이 URL 만 받던 시절의 내장 입력창. 위 폴백에서만 쓴다. */
279
+ let imageUrlPrompt = $state(false);
280
+
281
+ /*
282
+ * 모달이 돌려주는 네 가지 결말. 모달을 닫는 것도 여기서 한다 —
283
+ * 모달은 "무엇을 골랐는지"만 알리고 그 뒤는 모른다.
284
+ *
285
+ * ⚠️ 업로드는 **실패해도 모달을 닫는다.** 열어 둔 채로 두면 `업로드 중...` 오버레이가
286
+ * 모달 뒤에 깔려 무슨 일이 일어나는지 안 보인다. 실패는 아래 `catch` 가 알린다.
287
+ */
288
+ function insertImageFile(file: File) {
289
+ mediaPicker = null;
290
+ if (!editor || !onUploadFile) return;
291
+ uploading = true;
292
+ onUploadFile(file)
293
+ .then((url) => {
294
+ editor!.chain().focus().setImage({ src: url }).run();
295
+ })
296
+ .catch(() => {
297
+ alert("이미지 업로드에 실패했습니다.");
298
+ })
299
+ .finally(() => {
300
+ uploading = false;
301
+ });
302
+ }
303
+
304
+ function insertImageUrl(url: string) {
305
+ mediaPicker = null;
306
+ editor?.chain().focus().setImage({ src: url }).run();
307
+ }
308
+
309
+ function insertFileUpload(file: File) {
310
+ mediaPicker = null;
311
+ uploadFile(file);
312
+ }
313
+
314
+ function insertFileUrl(url: string) {
315
+ mediaPicker = null;
316
+ editor
317
+ ?.chain()
318
+ .focus()
319
+ .setFileAttachment({ src: url, name: fileNameFromUrl(url) })
320
+ .run();
321
+ }
241
322
  let lastEmittedHtml = content; // onChange로 내보낸 마지막 HTML (외부→내부 변경만 감지용)
242
323
  let tableObserver: MutationObserver | undefined;
243
324
 
@@ -413,10 +494,38 @@
413
494
  editor = new Editor({
414
495
  element: editorElement,
415
496
  extensions: [
497
+ /*
498
+ * `link`·`underline` 을 끄는 것은 **v3 이전의 뒷정리**다.
499
+ *
500
+ * ⚠️ StarterKit v2 에는 이 둘이 없어서 아래에 `Link`·`Underline` 을 따로 달았다.
501
+ * v3(3.22.x)부터 StarterKit 이 그것을 품으면서 같은 이름이 두 번 등록되고
502
+ * `Duplicate extension names found: ['link', 'underline']` 경고가 뜬다.
503
+ *
504
+ * 지금은 나중에 등록한 아래 `Link.configure` 가 이긴다 — 실측으로 `target=_blank`
505
+ * 와 `rel=noopener noreferrer` 가 붙는 것을 확인했다. 하지만 **그 우선순위는 계약이
506
+ * 아니다.** TipTap 의 해결 순서가 바뀌면 `openOnClick: false` 와 rel/target 이
507
+ * 조용히 빠진다. 읽기 전용 인스턴스가 한 화면에 26~27 개 서는 곳도 있어서(아래
508
+ * 안내문 주석 참고) 중복 등록과 경고가 그만큼 쌓인다.
509
+ *
510
+ * `codeBlock: false` 와 같은 꼴 — **StarterKit 에서는 끄고 자기 것을 단다** — 로 맞춘다.
511
+ */
512
+ /*
513
+ * `blockquote: false` 인 이유는 위 셋과 같다 — **끄고 자기 것을 단다.**
514
+ * StarterKit 의 인용문은 `> ` 로 만들어지는데, 노션에 맞춰 `> ` 는 토글에 내주고
515
+ * 인용문은 `" ` 로 옮겼다(`NotionBlockquote`). 입력 규칙은 `configure` 로 바꿀 수
516
+ * 없어서 확장을 갈아 끼우는 것 말고는 방법이 없다.
517
+ *
518
+ * ⚠️ 두 규칙이 같은 `> ` 를 노리게 두면 안 된다. 어느 쪽이 이길지는 확장 등록
519
+ * 순서에 달리게 되어, 같은 키를 쳤는데 인용문이 나올 때와 토글이 나올 때가 갈린다.
520
+ */
416
521
  StarterKit.configure({
417
522
  heading: { levels: [1, 2, 3] },
418
523
  codeBlock: false,
524
+ blockquote: false,
525
+ link: false,
526
+ underline: false,
419
527
  }),
528
+ NotionBlockquote,
420
529
  ...(extraExtensions.some((ext) => (ext as any).name === 'codeBlock')
421
530
  ? []
422
531
  : [CodeBlockLowlight.configure({
@@ -483,6 +592,7 @@
483
592
  ...(fileDownloadBaseUrl ? { downloadBaseUrl: fileDownloadBaseUrl } : {}),
484
593
  }),
485
594
  MbusVideo,
595
+ VideoEmbed,
486
596
  CardBlock.configure({ promptBackground: onPromptCardBackground ?? null }),
487
597
  /*
488
598
  * ⚠️ **수학은 여기서 등록한다.** 예전엔 `MathInline`/`MathDisplay` 를 내보내기만 하고
@@ -505,7 +615,9 @@
505
615
  Indent,
506
616
  FixedDetails,
507
617
  DetailsContent,
508
- DetailsSummary,
618
+ /* 토글 제목(`# > `)을 위해 `level` 을 붙인 판본. 옛 문서는 level 0 으로 읽힌다. */
619
+ LeveledDetailsSummary,
620
+ NotionToggleInputRule,
509
621
  ...extraExtensions,
510
622
  ...(onUploadFile
511
623
  ? [
@@ -685,11 +797,12 @@
685
797
  {editor}
686
798
  {features}
687
799
  {onPromptLink}
688
- {onPromptImage}
689
800
  {onPromptMbus}
801
+ {onPromptVideo}
690
802
  {toolbarEnd}
691
803
  onPdfClick={() => pdfInputEl?.click()}
692
- onFileClick={onUploadFile ? () => fileInputEl?.click() : undefined}
804
+ onImageClick={pickImage}
805
+ onFileClick={onUploadFile ? pickFile : undefined}
693
806
  />
694
807
  {/if}
695
808
 
@@ -728,15 +841,16 @@
728
841
  {editor}
729
842
  {features}
730
843
  {onPromptLink}
731
- {onPromptImage}
732
844
  {onPromptMbus}
845
+ {onPromptVideo}
733
846
  query={slashQuery}
734
847
  onClose={closeSlashMenu}
848
+ onImagePick={pickImage}
735
849
  onPdfUpload={onUploadFile && features.has('pdf')
736
850
  ? () => pdfInputEl?.click()
737
851
  : undefined}
738
852
  onFileUpload={onUploadFile && features.has('file')
739
- ? () => fileInputEl?.click()
853
+ ? pickFile
740
854
  : undefined}
741
855
  />
742
856
  </div>
@@ -747,6 +861,48 @@
747
861
  `features` 로 막지 않는다 — `$…$` 입력 규칙과 붙여넣기 변환은 feature 와 무관하게
748
862
  항상 살아 있어서, 그렇게 만든 수식을 고치려면 이 모달이 필요하다.
749
863
  -->
864
+ <!--
865
+ 이미지·파일 고르기. 둘이 **같은 모달**을 쓰고 문구만 다르다
866
+ (`MediaPickerModal` 주석 참고).
867
+ -->
868
+ {#if mediaPicker === 'image'}
869
+ <MediaPickerModal
870
+ title="이미지 추가"
871
+ accept="image/*"
872
+ uploadLabel="파일 업로드"
873
+ linkPlaceholder="이미지 링크 붙여넣기"
874
+ linkConfirmLabel="이미지 임베드"
875
+ linkHint="웹에 있는 모든 이미지와 호환됨"
876
+ onUpload={insertImageFile}
877
+ onLink={insertImageUrl}
878
+ onCancel={() => (mediaPicker = null)}
879
+ />
880
+ {/if}
881
+ {#if mediaPicker === 'file'}
882
+ <MediaPickerModal
883
+ title="파일 추가"
884
+ uploadLabel="파일을 선택하세요"
885
+ linkPlaceholder="파일 링크 붙여넣기"
886
+ linkConfirmLabel="파일 임베드"
887
+ onUpload={insertFileUpload}
888
+ onLink={insertFileUrl}
889
+ onCancel={() => (mediaPicker = null)}
890
+ />
891
+ {/if}
892
+
893
+ <!-- 업로드를 못 하는 호스트용 폴백(URL 만 받는다). 위 `pickImage` 참고. -->
894
+ {#if imageUrlPrompt}
895
+ <InputModal
896
+ title="이미지 URL 입력"
897
+ placeholder="https://example.com/image.png"
898
+ onConfirm={(url) => {
899
+ imageUrlPrompt = false;
900
+ editor?.chain().focus().setImage({ src: url }).run();
901
+ }}
902
+ onCancel={() => (imageUrlPrompt = false)}
903
+ />
904
+ {/if}
905
+
750
906
  {#if mathPrompt}
751
907
  <MathModal
752
908
  latex={mathPrompt.latex}
@@ -13,6 +13,8 @@ type $$ComponentProps = {
13
13
  onPromptLink?: PromptHandler;
14
14
  onPromptImage?: PromptHandler;
15
15
  onPromptMbus?: PromptHandler;
16
+ /** 영상(유튜브·Vimeo 등) URL 프롬프트. 미제공 시 내장 InputModal 폴백 */
17
+ onPromptVideo?: PromptHandler;
16
18
  /** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
17
19
  onPromptCardBackground?: PromptHandler;
18
20
  /** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA+GA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAuBtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AASjD,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAM9E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAwmBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA8FA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA4BtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAUjD,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQ9E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,uDAAuD;IACvD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAmvBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}