@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.
@@ -229,13 +229,19 @@
229
229
  uploading = true;
230
230
  const size = file.size;
231
231
  onUploadFile(file)
232
- .then((url) => {
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: { src: url, name: file.name, size },
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 = { resolver: onResolveFile };
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;AAkmBJ,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":"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;AA6BD,eAAO,MAAM,cAAc,kCAkMzB,CAAC"}
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 triggerDownload(e) {
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
- if (!resolvedSrc)
132
+ const proxyUrl = getProxyUrl();
133
+ const targetUrl = proxyUrl || resolvedSrc;
134
+ if (!targetUrl)
122
135
  return;
123
- fetch(resolvedSrc)
124
- .then((res) => res.blob())
125
- .then((blob) => {
126
- const url = URL.createObjectURL(blob);
127
- const a = document.createElement("a");
128
- a.href = url;
129
- a.download = resolvedName;
130
- a.click();
131
- URL.revokeObjectURL(url);
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", triggerDownload);
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 { resolver: null };
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,sBAwTnB,CAAC"}
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
- openLink.href = node.attrs.src;
250
- loadPdf(node.attrs.src);
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
- // resolver로 URL 획득
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',
@@ -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, tiptap-midibus → iframe으로 변환.
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":"AAyBA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcjD;AA0BD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyExD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAWrE"}
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"}
@@ -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, tiptap-midibus → iframe으로 변환.
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('converts <tiptap-midibus id="X"> to iframe', () => {
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('data-youtube-video');
29
- expect(result).toContain('https://play.mbus.tv/v1/hls/vid123');
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('removes iframe-wrapper divs after midibus conversion', () => {
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).not.toContain('iframe-wrapper');
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",