@teriusu/rich-editor 0.10.5 → 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.
- package/dist/components/TipTapEditor.svelte +32 -4
- package/dist/components/TipTapEditor.svelte.d.ts +15 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/LegacyBlock.d.ts +31 -2
- package/dist/extensions/LegacyBlock.d.ts.map +1 -1
- package/dist/extensions/LegacyBlock.js +28 -1
- package/dist/extensions/LegacyBlock.test.js +50 -0
- package/dist/extensions/TiptapMidibus.d.ts +31 -6
- package/dist/extensions/TiptapMidibus.d.ts.map +1 -1
- package/dist/extensions/TiptapMidibus.js +47 -10
- package/dist/extensions/TiptapMidibus.test.js +74 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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<
|
|
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;
|
|
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 는
|
|
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
|
-
* 것이라 패키지 안으로 들어올 수 없다.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
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
|
|
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 는
|
|
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
|
-
* 것이라 패키지 안으로 들어올 수 없다.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|