@teriusu/rich-editor 0.10.6 → 0.10.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.
@@ -192,8 +192,8 @@
192
192
  import { MbusVideo } from "../extensions/MbusVideo";
193
193
  import { ResizableImage } from "../extensions/ResizableImage";
194
194
  import { VideoEmbed } from "../extensions/VideoEmbed";
195
- import { TiptapMidibus } from "../extensions/TiptapMidibus";
196
- import { LegacyBlock } from "../extensions/LegacyBlock";
195
+ import { TiptapMidibus, type MidibusRenderer } from "../extensions/TiptapMidibus";
196
+ import { LegacyBlock, type LegacyBlockRenderer } from "../extensions/LegacyBlock";
197
197
  import { CardBlock } from "../extensions/CardBlock";
198
198
  import { MathInline, MathDisplay, type MathPrompt } from "../extensions/Math";
199
199
  import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
@@ -213,6 +213,8 @@
213
213
  onPromptVideo,
214
214
  onPromptCardBackground,
215
215
  onPromptMath,
216
+ onRenderMidibus,
217
+ onRenderLegacyBlock,
216
218
  toolbarEnd,
217
219
  extensions: extraExtensions = [],
218
220
  editable = true,
@@ -234,6 +236,19 @@
234
236
  onPromptCardBackground?: PromptHandler;
235
237
  /** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
236
238
  onPromptMath?: MathPrompt;
239
+ /**
240
+ * 정올 prod 형식 `<tiptap-midibus>` 를 **실제로 재생**할 플레이어.
241
+ *
242
+ * 패키지는 노드와 마크업만 책임지고, 플레이어는 호스트가 준다 — prod 플레이어가
243
+ * 로그인 계정 id·`GET /midibus/start/{id}`·PIP 헬퍼를 쓰기 때문이다(`resolver` 와
244
+ * 같은 패턴). 미제공 시 읽기 전용 자리표시자로 폴백한다.
245
+ */
246
+ onRenderMidibus?: MidibusRenderer;
247
+ /**
248
+ * 옛 블록(`lite-youtube`·`div.iframe-wrapper`·`div.tiptap-columns`)을 실제로 그릴 렌더러.
249
+ * `kind` 를 보고 아는 것만 그리고 나머지는 `false` 로 사양하면 된다.
250
+ */
251
+ onRenderLegacyBlock?: LegacyBlockRenderer;
237
252
  /** 고정 툴바 오른쪽 끝에 끼워 넣을 조각 */
238
253
  toolbarEnd?: Snippet;
239
254
  extensions?: AnyExtension[];
@@ -307,6 +322,19 @@
307
322
  pending?.resolve(value);
308
323
  }
309
324
 
325
+ /*
326
+ * 플레이어 주입 다리 둘. `promptMath` 와 **같은 이유로 함수를 직접 넘기지 않는다** —
327
+ * 확장은 `onMount` 에서 한 번만 만들어지므로, prop 을 그대로 넘기면 호스트가 나중에
328
+ * 붙인 렌더러를 영영 못 본다. 여기서 호출 시점에 읽는다.
329
+ *
330
+ * 안 준 호스트(코드패스)에게는 `false` 를 돌려 확장이 자리표시자로 되돌아가게 한다.
331
+ */
332
+ const renderMidibus: MidibusRenderer = (context) =>
333
+ onRenderMidibus ? onRenderMidibus(context) : false;
334
+
335
+ const renderLegacyBlock: LegacyBlockRenderer = (context) =>
336
+ onRenderLegacyBlock ? onRenderLegacyBlock(context) : false;
337
+
310
338
  let uploading = $state(false);
311
339
  let pdfInputEl: HTMLInputElement | undefined = $state();
312
340
  let fileInputEl: HTMLInputElement | undefined = $state();
@@ -682,8 +710,8 @@
682
710
  * `TiptapMidibus` 는 `<tiptap-midibus>` 를, `LegacyBlock` 은 `lite-youtube`·
683
711
  * `div.iframe-wrapper`·`div.tiptap-columns` 를 **원본 그대로** 읽고 되쓴다.
684
712
  */
685
- TiptapMidibus,
686
- LegacyBlock,
713
+ TiptapMidibus.configure({ renderer: renderMidibus }),
714
+ LegacyBlock.configure({ renderer: renderLegacyBlock }),
687
715
  CardBlock.configure({ promptBackground: onPromptCardBackground ?? null }),
688
716
  /*
689
717
  * ⚠️ **수학은 여기서 등록한다.** 예전엔 `MathInline`/`MathDisplay` 를 내보내기만 하고
@@ -1,5 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { AnyExtension } from "@tiptap/core";
3
+ import { type MidibusRenderer } from "../extensions/TiptapMidibus";
4
+ import { type LegacyBlockRenderer } from "../extensions/LegacyBlock";
3
5
  import { type MathPrompt } from "../extensions/Math";
4
6
  import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
5
7
  import type { FileResolver } from "../extensions/FileAttachment";
@@ -19,6 +21,19 @@ type $$ComponentProps = {
19
21
  onPromptCardBackground?: PromptHandler;
20
22
  /** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
21
23
  onPromptMath?: MathPrompt;
24
+ /**
25
+ * 정올 prod 형식 `<tiptap-midibus>` 를 **실제로 재생**할 플레이어.
26
+ *
27
+ * 패키지는 노드와 마크업만 책임지고, 플레이어는 호스트가 준다 — prod 플레이어가
28
+ * 로그인 계정 id·`GET /midibus/start/{id}`·PIP 헬퍼를 쓰기 때문이다(`resolver` 와
29
+ * 같은 패턴). 미제공 시 읽기 전용 자리표시자로 폴백한다.
30
+ */
31
+ onRenderMidibus?: MidibusRenderer;
32
+ /**
33
+ * 옛 블록(`lite-youtube`·`div.iframe-wrapper`·`div.tiptap-columns`)을 실제로 그릴 렌더러.
34
+ * `kind` 를 보고 아는 것만 그리고 나머지는 `false` 로 사양하면 된다.
35
+ */
36
+ onRenderLegacyBlock?: LegacyBlockRenderer;
22
37
  /** 고정 툴바 오른쪽 끝에 끼워 넣을 조각 */
23
38
  toolbarEnd?: Snippet;
24
39
  extensions?: AnyExtension[];
@@ -1 +1 @@
1
- {"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AA6JA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA2BtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAcjD,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9E,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;AAqxBJ,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":"AA6JA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA2BtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAWjD,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAElF,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9E,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;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,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;AAoyBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -20,15 +20,44 @@
20
20
  * 값 없는 속성(`allowfullscreen`)은 `allowfullscreen=""` 로 정규화된다. prod 저장본도
21
21
  * 같은 경로(`getHTML()`)로 만들어져 이미 `=""` 꼴이라 실사용에서는 어긋나지 않는다.
22
22
  */
23
- import { Node } from "@tiptap/core";
23
+ import { Node, type Editor } from "@tiptap/core";
24
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
24
25
  /** ProseMirror `DOMOutputSpec` 과 같은 모양이되 **JSON 으로 저장 가능한** 형태. */
25
26
  export type LegacySpec = string | LegacySpecElement;
26
27
  export type LegacySpecElement = [string, Record<string, string>, ...LegacySpec[]];
27
28
  export type LegacyBlockKind = "youtube" | "iframe" | "columns";
29
+ /**
30
+ * 호스트가 이 옛 블록을 **살려서 그리고 싶을 때** 받는 것.
31
+ *
32
+ * ⚠️ 세 종류가 한 노드에 묶여 있으므로 `kind` 를 보고 **아는 것만 그리고 나머지는
33
+ * `false` 로 사양**하면 된다. 정올 lms 는 `youtube` 만 그리고 `iframe`·`columns` 는
34
+ * 사양해 자리표시자를 그대로 쓴다.
35
+ */
36
+ export interface LegacyBlockRenderContext {
37
+ /** 그려 넣을 빈 칸. 노드뷰가 만들어 준다(자리표시자 테두리는 아직 안 붙었다). */
38
+ element: HTMLElement;
39
+ kind: LegacyBlockKind;
40
+ /** 저장된 원본 마크업. 여기서 `videoid`·`src` 를 꺼내 쓴다. */
41
+ spec: LegacySpecElement | null;
42
+ node: ProseMirrorNode;
43
+ editor: Editor;
44
+ }
45
+ /** 치울 함수를 돌려주거나, `false` 로 사양하면 자리표시자로 되돌아간다. */
46
+ export type LegacyBlockRenderResult = (() => void) | void | false;
47
+ export type LegacyBlockRenderer = (context: LegacyBlockRenderContext) => LegacyBlockRenderResult;
48
+ export interface LegacyBlockOptions {
49
+ /**
50
+ * 호스트가 심어 주는 실물 렌더러. 없거나 `false` 를 돌려주면 자리표시자.
51
+ *
52
+ * ⚠️ **주입은 화면만 바꾼다.** `renderHTML` 은 이 옵션을 보지 않고 `spec` 을 그대로
53
+ * 되뱉으므로, 렌더러가 있어도 왕복 바이트는 같다. 테스트로 박아 뒀다.
54
+ */
55
+ renderer: LegacyBlockRenderer | null;
56
+ }
28
57
  /**
29
58
  * DOM 요소를 저장 가능한 명세로 옮긴다. 속성은 **적힌 순서 그대로**, 자식은 텍스트까지
30
59
  * 그대로 옮긴다. `on*` 만 떨어뜨린다 — 그것까지 되쓰면 보존이 아니라 XSS 통로가 된다.
31
60
  */
32
61
  export declare function elementToSpec(el: Element): LegacySpecElement;
33
- export declare const LegacyBlock: Node<any, any>;
62
+ export declare const LegacyBlock: Node<LegacyBlockOptions, any>;
34
63
  //# sourceMappingURL=LegacyBlock.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LegacyBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/LegacyBlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGpC,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;AAElF,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAW/D;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,iBAAiB,CAqB5D;AA6DD,eAAO,MAAM,WAAW,gBAiFtB,CAAC"}
1
+ {"version":3,"file":"LegacyBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/LegacyBlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAiB,IAAI,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE/E,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;AAElF,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACxC,mDAAmD;IACnD,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,+CAA+C;IAC/C,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,iDAAiD;AACjD,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAClE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,wBAAwB,KAAK,uBAAuB,CAAC;AAEjG,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACrC;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,iBAAiB,CAqB5D;AA6DD,eAAO,MAAM,WAAW,+BA6GtB,CAAC"}
@@ -122,6 +122,9 @@ export const LegacyBlock = Node.create({
122
122
  atom: true,
123
123
  selectable: true,
124
124
  draggable: false,
125
+ addOptions() {
126
+ return { renderer: null };
127
+ },
125
128
  addAttributes() {
126
129
  return {
127
130
  /* 원본 마크업. `rendered: false` — `renderHTML` 이 직접 되뱉는다. */
@@ -152,13 +155,37 @@ export const LegacyBlock = Node.create({
152
155
  return cloneSpec(spec);
153
156
  },
154
157
  addNodeView() {
155
- return ({ node }) => {
158
+ return ({ node, editor }) => {
156
159
  const kind = (node.attrs.kind ?? "iframe");
157
160
  const spec = node.attrs.spec;
158
161
  const dom = document.createElement("div");
159
162
  dom.setAttribute("data-type", "legacyBlock");
160
163
  dom.setAttribute("data-legacy-kind", kind);
161
164
  dom.setAttribute("data-node-view-wrapper", "");
165
+ const renderer = this.options.renderer;
166
+ if (renderer) {
167
+ let cleanup = false;
168
+ try {
169
+ cleanup = renderer({ element: dom, kind, spec, node, editor });
170
+ }
171
+ catch {
172
+ /* 호스트 렌더러가 터져도 노드까지 잃지는 않는다. */
173
+ cleanup = false;
174
+ }
175
+ if (cleanup !== false) {
176
+ return {
177
+ dom,
178
+ /* `TiptapMidibus` 와 같은 이유. 주석은 그쪽에. */
179
+ update: () => false,
180
+ ignoreMutation: () => true,
181
+ destroy: () => {
182
+ if (typeof cleanup === "function")
183
+ cleanup();
184
+ }
185
+ };
186
+ }
187
+ dom.replaceChildren();
188
+ }
162
189
  dom.style.cssText =
163
190
  "margin:8px 0;padding:16px;border:1px dashed rgba(120,130,150,0.5);border-radius:8px;background:rgba(120,130,150,0.06);box-sizing:border-box;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;";
164
191
  const label = document.createElement("span");
@@ -92,4 +92,54 @@ describe('LegacyBlock — 옛 정올 블록 보존', () => {
92
92
  expect(clean).not.toContain('evil.example');
93
93
  });
94
94
  });
95
+ /*
96
+ * 호스트 렌더러 주입(정올 lms 는 `youtube` 만 그리고 나머지는 사양한다).
97
+ * **화면만 바뀌고 저장 바이트는 그대로여야 한다.**
98
+ */
99
+ describe('renderer 주입', () => {
100
+ function createWithRenderer(content, renderer) {
101
+ editor = new Editor({
102
+ element: document.createElement('div'),
103
+ extensions: [StarterKit, LegacyBlock.configure({ renderer }), Columns, Column],
104
+ content
105
+ });
106
+ return editor;
107
+ }
108
+ /** lms 가 실제로 쓰는 모양 — 유튜브만 그리고 나머지는 자리표시자에 맡긴다. */
109
+ const youtubeOnly = ({ element, kind, spec }) => {
110
+ if (kind !== 'youtube')
111
+ return false;
112
+ const videoid = spec?.[1]?.videoid ?? '';
113
+ element.innerHTML = `<div class="yt" data-videoid="${videoid}"></div>`;
114
+ };
115
+ it('셋을 함께 넣어도 왕복 바이트가 그대로다', () => {
116
+ const html = YOUTUBE + IFRAME + COLUMNS;
117
+ createWithRenderer(html, youtubeOnly);
118
+ expect(editor.getHTML()).toBe(html);
119
+ });
120
+ it('유튜브만 실물이 서고 나머지는 자리표시자다', () => {
121
+ createWithRenderer(YOUTUBE + IFRAME + COLUMNS, youtubeOnly);
122
+ const blocks = Array.from(editor.view.dom.querySelectorAll('[data-type="legacyBlock"]'));
123
+ const byKind = (kind) => blocks.find((el) => el.getAttribute('data-legacy-kind') === kind);
124
+ expect(byKind('youtube')?.querySelector('.yt')?.getAttribute('data-videoid')).toBe('dQw4w9WgXcQ');
125
+ expect(byKind('youtube')?.textContent).not.toContain('편집 불가');
126
+ expect(byKind('iframe')?.textContent).toContain('임베드 (옛 형식 · 편집 불가)');
127
+ expect(byKind('columns')?.textContent).toContain('단 나누기 (옛 형식 · 편집 불가)');
128
+ });
129
+ it('렌더러가 던져도 자리표시자로 살아남고 바이트도 그대로다', () => {
130
+ createWithRenderer(YOUTUBE, () => {
131
+ throw new Error('host boom');
132
+ });
133
+ expect(editor.view.dom.textContent).toContain('유튜브 영상 (옛 형식 · 편집 불가)');
134
+ expect(editor.getHTML()).toBe(YOUTUBE);
135
+ });
136
+ it('노드가 사라지면 치울 함수가 불린다', () => {
137
+ let destroyed = 0;
138
+ createWithRenderer(YOUTUBE, () => () => {
139
+ destroyed += 1;
140
+ });
141
+ editor.commands.setContent('<p>비움</p>');
142
+ expect(destroyed).toBe(1);
143
+ });
144
+ });
95
145
  });
@@ -13,20 +13,45 @@
13
13
  * 2. `renderHTML` 은 `rawAttrs` 가 있으면 그것을 그대로 되뱉는다 → 왕복에서 바이트 보존.
14
14
  * 3. 화면에는 **읽기 전용 자리표시자**만 세운다(아래 이유).
15
15
  *
16
- * ⚠️ **재생 UI 는 일부러 포팅하지 않았다.** prod 플레이어
16
+ * ⚠️ **재생 UI 는 패키지에 넣지 않는다 — 호스트가 주입한다.** prod 플레이어
17
17
  * (`trinity/apps/jungol/src/components/ui/tiptap/midibus/MidibusInner.svelte`)는
18
18
  * ① 로그인 계정 id 로 `uuid` 를 조립하고 ② `GET /midibus/start/{id}` 로 이어보기 지점을
19
19
  * 받아오며 ③ PIP 헬퍼를 쓴다 — 셋 다 **호스트 앱의 API 클라이언트·세션**이 있어야 하는
20
- * 것이라 패키지 안으로 들어올 수 없다. 게다가 정올은 전 문서에 `COEP: require-corp` 를
21
- * 걸어 두어서, `credentialless` 를 모르는 파이어폭스·사파리에서는 prod 에서도 이 영상이
22
- * 이미 **빈 회색 박스**다. 재생을 어설프게 옮기면 그 미해결 문제를 그대로 물려받는다.
23
- * 대신 자리표시자에 플레이어 새 탭 링크를 둔다 — 비로그인 prod 가 쓰는 것과 같은 주소다.
20
+ * 것이라 패키지 안으로 들어올 수 없다. 그래서 파일 첨부가 `resolver` 를 받는 것과 같은
21
+ * 방식으로 `renderer` 를 받는다: **노드와 마크업은 여기, 플레이어는 앱.**
22
+ * 주입이 없으면(코드패스 등) 지금까지처럼 자리표시자로 폴백한다.
23
+ *
24
+ * ⚠️ 정올은 전 문서에 `COEP: require-corp` 를 걸어 두어서, `credentialless` 를 모르는
25
+ * 파이어폭스·사파리에서는 **prod 에서도 이 영상이 이미 빈 회색 박스**다. 주입된
26
+ * 플레이어도 그 미해결 문제를 그대로 물려받는다 — 나빠지지는 않지만 낫지도 않다.
27
+ * 자리표시자에는 플레이어 새 탭 링크를 둔다 — 비로그인 prod 가 쓰는 것과 같은 주소다.
28
+ */
29
+ import { Node, type Editor } from "@tiptap/core";
30
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
31
+ /** 호스트 플레이어가 받는 것. 속성은 `node.attrs`(`id`·`start`·`uuid`·`width`·`height`). */
32
+ export interface MidibusRenderContext {
33
+ /** 플레이어를 그려 넣을 빈 칸. 노드뷰가 만들어 준다. */
34
+ element: HTMLElement;
35
+ node: ProseMirrorNode;
36
+ editor: Editor;
37
+ }
38
+ /**
39
+ * 치울 함수를 돌려주거나(노드가 사라질 때 불린다), `false` 로 **사양**한다.
40
+ * 사양하면 노드뷰가 자리표시자로 되돌아가므로, 호스트는 조건부로만 그릴 수 있다.
24
41
  */
25
- import { Node } from "@tiptap/core";
42
+ export type MidibusRenderResult = (() => void) | void | false;
43
+ export type MidibusRenderer = (context: MidibusRenderContext) => MidibusRenderResult;
26
44
  export interface TiptapMidibusOptions {
27
45
  HTMLAttributes: Record<string, unknown>;
28
46
  /** 자리표시자의 "새 탭에서 보기" 가 향할 플레이어 주소 앞부분. */
29
47
  playerBaseUrl: string;
48
+ /**
49
+ * 호스트가 심어 주는 진짜 플레이어. 없거나 `false` 를 돌려주면 자리표시자.
50
+ *
51
+ * ⚠️ **주입은 화면만 바꾼다.** `renderHTML`(=`getHTML()`)은 이 옵션을 보지 않으므로
52
+ * 왕복 바이트 보존은 렌더러가 있든 없든 같다. 테스트로 박아 뒀다.
53
+ */
54
+ renderer: MidibusRenderer | null;
30
55
  }
31
56
  declare module "@tiptap/core" {
32
57
  interface Commands<ReturnType> {
@@ -1 +1 @@
1
- {"version":3,"file":"TiptapMidibus.d.ts","sourceRoot":"","sources":["../../src/extensions/TiptapMidibus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;CACtB;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,iCAgHxB,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;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"}
@@ -13,14 +13,18 @@
13
13
  * 2. `renderHTML` 은 `rawAttrs` 가 있으면 그것을 그대로 되뱉는다 → 왕복에서 바이트 보존.
14
14
  * 3. 화면에는 **읽기 전용 자리표시자**만 세운다(아래 이유).
15
15
  *
16
- * ⚠️ **재생 UI 는 일부러 포팅하지 않았다.** prod 플레이어
16
+ * ⚠️ **재생 UI 는 패키지에 넣지 않는다 — 호스트가 주입한다.** prod 플레이어
17
17
  * (`trinity/apps/jungol/src/components/ui/tiptap/midibus/MidibusInner.svelte`)는
18
18
  * ① 로그인 계정 id 로 `uuid` 를 조립하고 ② `GET /midibus/start/{id}` 로 이어보기 지점을
19
19
  * 받아오며 ③ PIP 헬퍼를 쓴다 — 셋 다 **호스트 앱의 API 클라이언트·세션**이 있어야 하는
20
- * 것이라 패키지 안으로 들어올 수 없다. 게다가 정올은 전 문서에 `COEP: require-corp` 를
21
- * 걸어 두어서, `credentialless` 를 모르는 파이어폭스·사파리에서는 prod 에서도 이 영상이
22
- * 이미 **빈 회색 박스**다. 재생을 어설프게 옮기면 그 미해결 문제를 그대로 물려받는다.
23
- * 대신 자리표시자에 플레이어 새 탭 링크를 둔다 — 비로그인 prod 가 쓰는 것과 같은 주소다.
20
+ * 것이라 패키지 안으로 들어올 수 없다. 그래서 파일 첨부가 `resolver` 를 받는 것과 같은
21
+ * 방식으로 `renderer` 를 받는다: **노드와 마크업은 여기, 플레이어는 앱.**
22
+ * 주입이 없으면(코드패스 등) 지금까지처럼 자리표시자로 폴백한다.
23
+ *
24
+ * ⚠️ 정올은 전 문서에 `COEP: require-corp` 를 걸어 두어서, `credentialless` 를 모르는
25
+ * 파이어폭스·사파리에서는 **prod 에서도 이 영상이 이미 빈 회색 박스**다. 주입된
26
+ * 플레이어도 그 미해결 문제를 그대로 물려받는다 — 나빠지지는 않지만 낫지도 않다.
27
+ * 자리표시자에는 플레이어 새 탭 링크를 둔다 — 비로그인 prod 가 쓰는 것과 같은 주소다.
24
28
  */
25
29
  import { Node, mergeAttributes } from "@tiptap/core";
26
30
  /**
@@ -47,7 +51,8 @@ export const TiptapMidibus = Node.create({
47
51
  addOptions() {
48
52
  return {
49
53
  HTMLAttributes: {},
50
- playerBaseUrl: "https://play.mbus.tv/v1/hls"
54
+ playerBaseUrl: "https://play.mbus.tv/v1/hls",
55
+ renderer: null
51
56
  };
52
57
  },
53
58
  /*
@@ -92,14 +97,46 @@ export const TiptapMidibus = Node.create({
92
97
  ];
93
98
  },
94
99
  addNodeView() {
95
- return ({ node }) => {
96
- const videoId = String(node.attrs.id ?? "").trim();
97
- const startRaw = Number(node.attrs.start ?? 0);
98
- const start = Number.isFinite(startRaw) ? Math.max(0, Math.floor(startRaw)) : 0;
100
+ return ({ node, editor }) => {
99
101
  const dom = document.createElement("div");
100
102
  dom.setAttribute("data-type", "tiptapMidibus");
101
103
  dom.setAttribute("data-node-view-wrapper", "");
102
104
  dom.style.cssText = "margin:8px 0;position:relative;box-sizing:border-box;max-width:100%;";
105
+ const renderer = this.options.renderer;
106
+ if (renderer) {
107
+ let cleanup = false;
108
+ try {
109
+ cleanup = renderer({ element: dom, node, editor });
110
+ }
111
+ catch {
112
+ /* 호스트 플레이어가 터져도 노드까지 잃지는 않는다 — 자리표시자로 되돌아간다. */
113
+ cleanup = false;
114
+ }
115
+ if (cleanup !== false) {
116
+ return {
117
+ dom,
118
+ /*
119
+ * 속성이 바뀌면 노드뷰를 **새로 만들게 둔다**(`false`). 호스트가 어떤
120
+ * 틀로 그렸는지 여기서는 모르므로 갈아 끼울 방법이 없다.
121
+ */
122
+ update: () => false,
123
+ /*
124
+ * 호스트가 그린 DOM 은 **문서가 아니다.** 재생 중 iframe·버튼이
125
+ * 바뀌는 것을 파서가 되읽으면 노드 속성이 오염된다.
126
+ */
127
+ ignoreMutation: () => true,
128
+ destroy: () => {
129
+ if (typeof cleanup === "function")
130
+ cleanup();
131
+ }
132
+ };
133
+ }
134
+ /* 사양했다. 호스트가 뭔가 그리다 말았을 수 있으니 비우고 자리표시자로 간다. */
135
+ dom.replaceChildren();
136
+ }
137
+ const videoId = String(node.attrs.id ?? "").trim();
138
+ const startRaw = Number(node.attrs.start ?? 0);
139
+ const start = Number.isFinite(startRaw) ? Math.max(0, Math.floor(startRaw)) : 0;
103
140
  const aspect = document.createElement("div");
104
141
  aspect.style.cssText =
105
142
  "position:relative;width:100%;padding-top:56.25%;background:#0b1020;border-radius:8px;overflow:hidden;";
@@ -86,4 +86,78 @@ describe('TiptapMidibus — prod 저장 형식 왕복', () => {
86
86
  expect(clean).toContain('data-bubble-menu="false"');
87
87
  expect(clean).toContain('</tiptap-midibus>');
88
88
  });
89
+ /*
90
+ * 호스트 플레이어 주입. **화면만 바뀌고 저장 바이트는 그대로여야 한다** — 이게
91
+ * 깨지면 lms 에서 열었다 저장하는 것만으로 prod 쪽 렌더가 어긋난다.
92
+ */
93
+ describe('renderer 주입', () => {
94
+ function createWithRenderer(content, renderer) {
95
+ editor = new Editor({
96
+ element: document.createElement('div'),
97
+ extensions: [StarterKit, TiptapMidibus.configure({ renderer })],
98
+ content
99
+ });
100
+ return editor;
101
+ }
102
+ it('주입해도 왕복 바이트가 그대로다', () => {
103
+ createWithRenderer(PROD, ({ element }) => {
104
+ element.appendChild(document.createElement('iframe'));
105
+ });
106
+ expect(editor.getHTML()).toBe(PROD);
107
+ });
108
+ it('모르는 속성이 붙어 있어도 주입이 바이트를 건드리지 않는다', () => {
109
+ const html = '<tiptap-midibus id="v" start="12" uuid="u" width="100%" height="600" data-bubble-menu="false" data-resize-aspect-ratio="1.777"></tiptap-midibus>';
110
+ createWithRenderer(html, ({ element }) => {
111
+ element.innerHTML = '<div class="player">재생 중</div>';
112
+ });
113
+ expect(editor.getHTML()).toBe(html);
114
+ });
115
+ /*
116
+ * ⚠️ **`height`·`start` 는 숫자로 들어온다.** TipTap 이 숫자꼴 속성값을 알아서
117
+ * 바꾼다 — 호스트 플레이어는 문자열이라고 단정하면 안 된다(정올 `MidibusInner`
118
+ * 는 둘 다 받도록 `Number.parseFloat(String(...))` 로 읽는다).
119
+ */
120
+ it('렌더러가 노드 속성을 받는다', () => {
121
+ let seen = null;
122
+ createWithRenderer(PROD, ({ node }) => {
123
+ seen = { ...node.attrs };
124
+ });
125
+ expect(seen).toMatchObject({ id: 'abc123', uuid: 'abc123', height: 600 });
126
+ });
127
+ it('주입하면 자리표시자 대신 호스트 DOM 이 선다', () => {
128
+ createWithRenderer(PROD, ({ element }) => {
129
+ element.setAttribute('data-host-player', '1');
130
+ });
131
+ const dom = editor.view.dom.querySelector('[data-type="tiptapMidibus"]');
132
+ expect(dom?.getAttribute('data-host-player')).toBe('1');
133
+ expect(dom?.textContent).not.toContain('새 탭에서 보기');
134
+ });
135
+ it('false 로 사양하면 자리표시자로 되돌아간다', () => {
136
+ createWithRenderer(PROD, ({ element }) => {
137
+ /* 그리다 만 흔적을 남겨도 자리표시자가 깨끗해야 한다. */
138
+ element.appendChild(document.createElement('span'));
139
+ return false;
140
+ });
141
+ const dom = editor.view.dom.querySelector('[data-type="tiptapMidibus"]');
142
+ expect(dom?.textContent).toContain('새 탭에서 보기');
143
+ expect(editor.getHTML()).toBe(PROD);
144
+ });
145
+ /* 호스트가 터져도 노드까지 잃으면 안 된다 — 보존이 최우선 계약이다. */
146
+ it('렌더러가 던져도 자리표시자로 살아남고 바이트도 그대로다', () => {
147
+ createWithRenderer(PROD, () => {
148
+ throw new Error('host boom');
149
+ });
150
+ const dom = editor.view.dom.querySelector('[data-type="tiptapMidibus"]');
151
+ expect(dom?.textContent).toContain('강의 영상');
152
+ expect(editor.getHTML()).toBe(PROD);
153
+ });
154
+ it('노드가 사라지면 치울 함수가 불린다', () => {
155
+ let destroyed = 0;
156
+ createWithRenderer(PROD, () => () => {
157
+ destroyed += 1;
158
+ });
159
+ editor.commands.setContent('<p>비움</p>');
160
+ expect(destroyed).toBe(1);
161
+ });
162
+ });
89
163
  });
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@ export { FileAttachment } from "./extensions/FileAttachment";
6
6
  export type { FileResolver, FileResolveResult } from "./extensions/FileAttachment";
7
7
  export { MbusVideo } from "./extensions/MbusVideo";
8
8
  export { TiptapMidibus } from "./extensions/TiptapMidibus";
9
- export type { TiptapMidibusOptions, MidibusRawAttrs } from "./extensions/TiptapMidibus";
9
+ export type { TiptapMidibusOptions, MidibusRawAttrs, MidibusRenderContext, MidibusRenderResult, MidibusRenderer } from "./extensions/TiptapMidibus";
10
10
  export { LegacyBlock, elementToSpec } from "./extensions/LegacyBlock";
11
- export type { LegacySpec, LegacySpecElement, LegacyBlockKind } from "./extensions/LegacyBlock";
11
+ export type { LegacySpec, LegacySpecElement, LegacyBlockKind, LegacyBlockOptions, LegacyBlockRenderContext, LegacyBlockRenderResult, LegacyBlockRenderer } from "./extensions/LegacyBlock";
12
12
  export { ResizableImage, normalizeImageWidth } from "./extensions/ResizableImage";
13
13
  export { CardBlock } from "./extensions/CardBlock";
14
14
  export type { CardBackgroundPrompt, CardBlockOptions } from "./extensions/CardBlock";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAW3E,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACxF,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACtE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGjE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAahC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG5D,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAW3E,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACtE,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGjE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAahC,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG5D,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",