@teriusu/rich-editor 0.5.0 → 0.5.3
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 +12 -3
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.d.ts.map +1 -1
- package/dist/extensions/FileAttachment.js +35 -14
- package/dist/extensions/PdfBlock.d.ts.map +1 -1
- package/dist/extensions/PdfBlock.js +19 -14
- package/dist/types.js +1 -1
- package/dist/utils/sanitize.d.ts +1 -1
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +3 -8
- package/dist/utils/sanitize.test.js +5 -7
- package/package.json +1 -1
|
@@ -229,13 +229,19 @@
|
|
|
229
229
|
uploading = true;
|
|
230
230
|
const size = file.size;
|
|
231
231
|
onUploadFile(file)
|
|
232
|
-
.then((
|
|
232
|
+
.then((result) => {
|
|
233
|
+
const isFileId = result && !result.includes("/") && !result.includes(":");
|
|
233
234
|
editor!
|
|
234
235
|
.chain()
|
|
235
236
|
.focus()
|
|
236
237
|
.insertContent({
|
|
237
238
|
type: "fileAttachment",
|
|
238
|
-
attrs: {
|
|
239
|
+
attrs: {
|
|
240
|
+
src: isFileId ? null : result,
|
|
241
|
+
fileId: isFileId ? result : null,
|
|
242
|
+
name: file.name,
|
|
243
|
+
size,
|
|
244
|
+
},
|
|
239
245
|
})
|
|
240
246
|
.run();
|
|
241
247
|
})
|
|
@@ -486,7 +492,10 @@
|
|
|
486
492
|
|
|
487
493
|
// File resolver를 storage에 등록
|
|
488
494
|
if (onResolveFile) {
|
|
489
|
-
editor.storage.fileAttachment = {
|
|
495
|
+
editor.storage.fileAttachment = {
|
|
496
|
+
...editor.storage.fileAttachment,
|
|
497
|
+
resolver: onResolveFile,
|
|
498
|
+
};
|
|
490
499
|
}
|
|
491
500
|
|
|
492
501
|
editor.on("update", handleUpdate);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3E,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,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;
|
|
1
|
+
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAa5D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3E,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,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;AA2mBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;
|
|
1
|
+
{"version":3,"file":"FileAttachment.d.ts","sourceRoot":"","sources":["../../src/extensions/FileAttachment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE1E,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,cAAc,EAAE;YACd,iBAAiB,EAAE,CAAC,KAAK,EAAE;gBACzB,GAAG,CAAC,EAAE,MAAM,CAAC;gBACb,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,IAAI,EAAE,MAAM,CAAC;gBACb,IAAI,CAAC,EAAE,MAAM,CAAC;aACf,KAAK,UAAU,CAAC;SAClB,CAAC;KACH;CACF;AAkCD,eAAO,MAAM,cAAc,kCAmNzB,CAAC"}
|
|
@@ -6,6 +6,10 @@ function formatFileSize(bytes) {
|
|
|
6
6
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7
7
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
8
8
|
}
|
|
9
|
+
function isInlineable(name) {
|
|
10
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
11
|
+
return ["pdf", "png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext);
|
|
12
|
+
}
|
|
9
13
|
function getFileIcon(name) {
|
|
10
14
|
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
11
15
|
const icons = {
|
|
@@ -116,22 +120,36 @@ export const FileAttachment = Node.create({
|
|
|
116
120
|
"display:block;font-size:13px;font-weight:600;color:var(--primary, #4A7DAC);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;";
|
|
117
121
|
let resolvedSrc = null;
|
|
118
122
|
let resolvedName = node.attrs.name;
|
|
119
|
-
function
|
|
123
|
+
function getProxyUrl() {
|
|
124
|
+
const fileId = node.attrs.fileId;
|
|
125
|
+
if (!fileId)
|
|
126
|
+
return null;
|
|
127
|
+
const baseUrl = editor.storage.fileAttachment?.downloadBaseUrl || "/api/upload";
|
|
128
|
+
return `${baseUrl}/${fileId}/download`;
|
|
129
|
+
}
|
|
130
|
+
function handleClick(e) {
|
|
120
131
|
e.preventDefault();
|
|
121
|
-
|
|
132
|
+
const proxyUrl = getProxyUrl();
|
|
133
|
+
const targetUrl = proxyUrl || resolvedSrc;
|
|
134
|
+
if (!targetUrl)
|
|
122
135
|
return;
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
if (isInlineable(resolvedName)) {
|
|
137
|
+
window.open(targetUrl, "_blank");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
fetch(targetUrl)
|
|
141
|
+
.then((res) => res.blob())
|
|
142
|
+
.then((blob) => {
|
|
143
|
+
const url = URL.createObjectURL(blob);
|
|
144
|
+
const a = document.createElement("a");
|
|
145
|
+
a.href = url;
|
|
146
|
+
a.download = resolvedName;
|
|
147
|
+
a.click();
|
|
148
|
+
URL.revokeObjectURL(url);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
133
151
|
}
|
|
134
|
-
nameEl.addEventListener("click",
|
|
152
|
+
nameEl.addEventListener("click", handleClick);
|
|
135
153
|
const sizeEl = document.createElement("span");
|
|
136
154
|
sizeEl.style.cssText =
|
|
137
155
|
"font-size:11px;color:var(--muted-foreground, #718096);";
|
|
@@ -197,7 +215,10 @@ export const FileAttachment = Node.create({
|
|
|
197
215
|
};
|
|
198
216
|
},
|
|
199
217
|
addStorage() {
|
|
200
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
resolver: null,
|
|
220
|
+
downloadBaseUrl: "/api/upload",
|
|
221
|
+
};
|
|
201
222
|
},
|
|
202
223
|
addCommands() {
|
|
203
224
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAInE,eAAO,MAAM,QAAQ,
|
|
1
|
+
{"version":3,"file":"PdfBlock.d.ts","sourceRoot":"","sources":["../../src/extensions/PdfBlock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,UAAU,EAAmB,MAAM,cAAc,CAAC;AAInE,eAAO,MAAM,QAAQ,sBA8TnB,CAAC"}
|
|
@@ -245,31 +245,36 @@ export const PdfBlock = TiptapNode.create({
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
// URL 해결 후 PDF 로드
|
|
248
|
+
function getProxyUrl(fileId) {
|
|
249
|
+
const baseUrl = editor.storage.fileAttachment?.downloadBaseUrl || "/api/upload";
|
|
250
|
+
return `${baseUrl}/${fileId}/download`;
|
|
251
|
+
}
|
|
248
252
|
if (node.attrs.src) {
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
if (node.attrs.fileId) {
|
|
254
|
+
// fileId 있으면 프록시 URL 사용 (경로 숨김)
|
|
255
|
+
const proxyUrl = getProxyUrl(node.attrs.fileId);
|
|
256
|
+
openLink.href = proxyUrl;
|
|
257
|
+
loadPdf(proxyUrl);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
openLink.href = node.attrs.src;
|
|
261
|
+
loadPdf(node.attrs.src);
|
|
262
|
+
}
|
|
251
263
|
}
|
|
252
264
|
else if (node.attrs.fileId) {
|
|
253
|
-
|
|
254
|
-
openLink.href =
|
|
265
|
+
const proxyUrl = getProxyUrl(node.attrs.fileId);
|
|
266
|
+
openLink.href = proxyUrl;
|
|
267
|
+
// resolver로 이름 획득
|
|
255
268
|
const resolver = editor.storage.fileAttachment?.resolver;
|
|
256
269
|
if (resolver) {
|
|
257
270
|
resolver(node.attrs.fileId)
|
|
258
271
|
.then((result) => {
|
|
259
|
-
openLink.href = result.src;
|
|
260
272
|
if (result.name)
|
|
261
273
|
nameSpan.textContent = result.name;
|
|
262
|
-
loadPdf(result.src);
|
|
263
274
|
})
|
|
264
|
-
.catch(() => {
|
|
265
|
-
loadingDiv.innerHTML =
|
|
266
|
-
'<p class="text-sm text-muted-foreground">PDF URL\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p>';
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
loadingDiv.innerHTML =
|
|
271
|
-
`<p class="text-sm text-muted-foreground">PDF ID: ${node.attrs.fileId}</p>`;
|
|
275
|
+
.catch(() => { });
|
|
272
276
|
}
|
|
277
|
+
loadPdf(proxyUrl);
|
|
273
278
|
}
|
|
274
279
|
return {
|
|
275
280
|
dom,
|
package/dist/types.js
CHANGED
|
@@ -20,7 +20,7 @@ export const TOOLBAR_PRESETS = {
|
|
|
20
20
|
'blockquote', 'horizontal-rule', 'toggle',
|
|
21
21
|
'link', 'image', 'pdf', 'youtube', 'video', 'file',
|
|
22
22
|
'columns-2', 'columns-3', 'table', 'code-block',
|
|
23
|
-
'bubble-toolbar', 'slash-menu', 'table-menu'
|
|
23
|
+
'fixed-toolbar', 'bubble-toolbar', 'slash-menu', 'table-menu'
|
|
24
24
|
],
|
|
25
25
|
minimal: [
|
|
26
26
|
'bold', 'italic', 'strike', 'code',
|
package/dist/utils/sanitize.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare function sanitizeHtml(html: string): string;
|
|
|
2
2
|
/**
|
|
3
3
|
* 레거시 TipTap v2 커스텀 태그를 현재 형식으로 변환.
|
|
4
4
|
* 에디터 content 로드 전, 또는 게시물 렌더링 전에 호출.
|
|
5
|
-
* tiptap-file → data-file-id
|
|
5
|
+
* tiptap-file → data-file-id 변환. tiptap-midibus는 호스트 앱이 처리.
|
|
6
6
|
*/
|
|
7
7
|
export declare function transformLegacyHtml(html: string): string;
|
|
8
8
|
export declare function stripHtmlToExcerpt(html: string, maxLen?: number): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize.ts"],"names":[],"mappings":"AA2BA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcjD;AA0BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4DxD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAWrE"}
|
package/dist/utils/sanitize.js
CHANGED
|
@@ -5,6 +5,7 @@ const ALLOWED_TAGS = new Set([
|
|
|
5
5
|
"blockquote", "pre", "code", "img", "figure", "figcaption",
|
|
6
6
|
"table", "colgroup", "col", "thead", "tbody", "tr", "th", "td",
|
|
7
7
|
"span", "div", "hr", "sub", "sup",
|
|
8
|
+
"tiptap-midibus",
|
|
8
9
|
]);
|
|
9
10
|
const ALLOWED_ATTRS = {
|
|
10
11
|
a: new Set(["href", "title", "target", "rel"]),
|
|
@@ -16,6 +17,7 @@ const ALLOWED_ATTRS = {
|
|
|
16
17
|
div: new Set(["data-pdf-src", "data-pdf-name"]),
|
|
17
18
|
pre: new Set(["class"]),
|
|
18
19
|
code: new Set(["class"]),
|
|
20
|
+
"tiptap-midibus": new Set(["id", "start", "uuid", "width", "height"]),
|
|
19
21
|
"*": new Set(["class", "id"]),
|
|
20
22
|
};
|
|
21
23
|
const SAFE_URL_PATTERN = /^(?:https?:\/\/|\/[\w])/i;
|
|
@@ -58,7 +60,7 @@ function sanitizeAttributes(tag, attrString) {
|
|
|
58
60
|
/**
|
|
59
61
|
* 레거시 TipTap v2 커스텀 태그를 현재 형식으로 변환.
|
|
60
62
|
* 에디터 content 로드 전, 또는 게시물 렌더링 전에 호출.
|
|
61
|
-
* tiptap-file → data-file-id
|
|
63
|
+
* tiptap-file → data-file-id 변환. tiptap-midibus는 호스트 앱이 처리.
|
|
62
64
|
*/
|
|
63
65
|
export function transformLegacyHtml(html) {
|
|
64
66
|
if (!html)
|
|
@@ -87,13 +89,6 @@ export function transformLegacyHtml(html) {
|
|
|
87
89
|
const name = text.trim() || "\uD30C\uC77C";
|
|
88
90
|
return `<div data-file-id="${id}" data-file-name="${name}"></div>`;
|
|
89
91
|
})
|
|
90
|
-
// <tiptap-midibus id="X" ...></tiptap-midibus> + 뒤따르는 iframe-wrapper 제거
|
|
91
|
-
// → <div data-youtube-video><iframe src="https://play.mbus.tv/v1/hls/X" allowfullscreen></iframe></div>
|
|
92
|
-
.replace(/<tiptap-midibus\s+id="([^"]*)"[^>]*>(?:<\/tiptap-midibus>)?/gi, (_match, id) => {
|
|
93
|
-
return `<div data-youtube-video=""><iframe src="https://play.mbus.tv/v1/hls/${id}" width="100%" height="400" frameborder="0" allowfullscreen="true"></iframe></div>`;
|
|
94
|
-
})
|
|
95
|
-
// 미디버스 뒤에 따라오는 빈 iframe-wrapper 제거
|
|
96
|
-
.replace(/<div\s+class="iframe-wrapper"[^>]*>[\s\S]*?<\/div>/gi, "")
|
|
97
92
|
// <div class="tiptap-columns"> → <div data-type="columns">
|
|
98
93
|
.replace(/<div\s+class="tiptap-columns">/gi, '<div data-type="columns">')
|
|
99
94
|
// <div class="tiptap-column"> → <div data-type="column">
|
|
@@ -22,13 +22,11 @@ describe('transformLegacyHtml', () => {
|
|
|
22
22
|
const result = transformLegacyHtml(input);
|
|
23
23
|
expect(result).toContain('data-file-name="파일"');
|
|
24
24
|
});
|
|
25
|
-
it('
|
|
25
|
+
it('preserves <tiptap-midibus> for host app custom extension', () => {
|
|
26
26
|
const input = '<tiptap-midibus id="vid123"></tiptap-midibus>';
|
|
27
27
|
const result = transformLegacyHtml(input);
|
|
28
|
-
expect(result).toContain('
|
|
29
|
-
expect(result).toContain('
|
|
30
|
-
expect(result).toContain('iframe');
|
|
31
|
-
expect(result).not.toContain('tiptap-midibus');
|
|
28
|
+
expect(result).toContain('tiptap-midibus');
|
|
29
|
+
expect(result).toContain('id="vid123"');
|
|
32
30
|
});
|
|
33
31
|
it('converts <tiptap-collapsable title="X"> to <details><summary>', () => {
|
|
34
32
|
const input = '<tiptap-collapsable title="Click me">Hidden content</tiptap-collapsable>';
|
|
@@ -73,10 +71,10 @@ describe('transformLegacyHtml', () => {
|
|
|
73
71
|
expect(result).toContain('Before');
|
|
74
72
|
expect(result).toContain('After');
|
|
75
73
|
});
|
|
76
|
-
it('
|
|
74
|
+
it('preserves midibus with trailing iframe-wrapper intact', () => {
|
|
77
75
|
const input = '<tiptap-midibus id="vid"></tiptap-midibus><div class="iframe-wrapper"><iframe></iframe></div>';
|
|
78
76
|
const result = transformLegacyHtml(input);
|
|
79
|
-
expect(result).
|
|
77
|
+
expect(result).toContain('tiptap-midibus');
|
|
80
78
|
});
|
|
81
79
|
it('parses margin-left: 40px via Indent extension (not transformLegacyHtml itself)', () => {
|
|
82
80
|
// transformLegacyHtml does NOT handle margin-left; the Indent extension does.
|