@teriusu/rich-editor 0.11.3 → 0.11.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAUrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BAmLpB,CAAC"}
1
+ {"version":3,"file":"MbusVideo.d.ts","sourceRoot":"","sources":["../../src/extensions/MbusVideo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAWrD,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,SAAS,EAAE;YACV,YAAY,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACrE,CAAC;KACF;CACD;AAED,eAAO,MAAM,SAAS,6BA0NpB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Node, mergeAttributes } from "@tiptap/core";
2
2
  import { attachResize } from "../utils/resize";
3
+ import { applyCrossOriginEmbedAttrs, canEmbedCrossOrigin } from "../utils/embed";
3
4
  import { getEditorTranslator } from "../i18n";
4
5
  import { mediaRatioCss, normalizeMediaAlign, normalizeMediaHeight, normalizeMediaRatio } from "../utils/media-size";
5
6
  export const MbusVideo = Node.create({
@@ -99,16 +100,53 @@ export const MbusVideo = Node.create({
99
100
  dom.style.marginRight = align === "center" ? "auto" : "";
100
101
  };
101
102
  applyLayout(node.attrs);
102
- if (node.attrs.src) {
103
+ /*
104
+ * ⚠️ **`credentialless` 를 반드시 붙인다** — `VideoEmbed` 와 같은 이유·같은
105
+ * 레시피다(`utils/embed.ts`). 정올은 전 문서가 `COEP: require-corp` 라, 이
106
+ * 속성이 없으면 mbus iframe 이 **크롬에서도 통째로 막히고** 그 자리에 크롬의
107
+ * 오류 화면(`play.mbus.tv 에서 연결을 거부했습니다`)이 뜬다.
108
+ *
109
+ * 이 노드만 그 처리가 빠져 있었다. 저장한 뒤 읽는 화면은 호스트의 정적 렌더
110
+ * (`trinity …/tiptap/static-enhance.ts` 의 `buildVideoBox`)가 같은 상자를 다시
111
+ * 지으면서 속성을 붙이므로 **에디터에서만** 거부로 보였다 — "영상을 올리면
112
+ * 연결이 거부된다"는 신고의 실체가 이 비대칭이다.
113
+ * (브라우저 실측, 격리된 문서: 속성 없는 mbus iframe = COEP 위반 보고 1건,
114
+ * 속성 있는 쪽 = 0건.)
115
+ */
116
+ if (node.attrs.src && canEmbedCrossOrigin()) {
103
117
  const iframe = document.createElement("iframe");
104
118
  iframe.src = node.attrs.src;
105
119
  iframe.allow = "autoplay; fullscreen; encrypted-media; picture-in-picture";
106
120
  iframe.setAttribute("allowfullscreen", "");
107
121
  iframe.setAttribute("loading", "lazy");
122
+ applyCrossOriginEmbedAttrs(iframe);
108
123
  iframe.style.cssText =
109
124
  "position:absolute;inset:0;width:100%;height:100%;border:0;display:block;";
110
125
  aspect.appendChild(iframe);
111
126
  }
127
+ else if (node.attrs.src) {
128
+ /*
129
+ * 실을 수 없는 브라우저(파폭·사파리 + 격리) — **빈 박스로 두지 않는다.**
130
+ * 그러면 영상이 깨진 줄 알지 이유를 모른다. `VideoEmbed`·호스트 정적 렌더와
131
+ * **같은 문구·같은 모양**의 포스터를 세운다.
132
+ */
133
+ const poster = document.createElement("div");
134
+ poster.style.cssText =
135
+ "position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:16px;text-align:center;color:#c7cbd4;";
136
+ const label = document.createElement("span");
137
+ label.textContent = t("videoCannotEmbed");
138
+ label.style.cssText = "font-size:13px;line-height:1.5;";
139
+ poster.appendChild(label);
140
+ const open = document.createElement("a");
141
+ open.href = node.attrs.src;
142
+ open.target = "_blank";
143
+ open.rel = "noopener noreferrer";
144
+ open.textContent = t("openInNewTab");
145
+ open.style.cssText =
146
+ "font-size:13px;font-weight:600;color:#fff;background:rgba(255,255,255,0.16);border-radius:6px;padding:6px 14px;text-decoration:none;";
147
+ poster.appendChild(open);
148
+ aspect.appendChild(poster);
149
+ }
112
150
  if (editor.isEditable) {
113
151
  const del = document.createElement("button");
114
152
  del.type = "button";
@@ -1 +1 @@
1
- {"version":3,"file":"VideoEmbed.d.ts","sourceRoot":"","sources":["../../src/extensions/VideoEmbed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAWrD,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,UAAU,EAAE;YACX,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACtE,CAAC;KACF;CACD;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAmBD,eAAO,MAAM,UAAU,8BAmSrB,CAAC"}
1
+ {"version":3,"file":"VideoEmbed.d.ts","sourceRoot":"","sources":["../../src/extensions/VideoEmbed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAYrD,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,QAAQ,CAAC,UAAU;QAC5B,UAAU,EAAE;YACX,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,UAAU,CAAC;SACtE,CAAC;KACF;CACD;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAOD,eAAO,MAAM,UAAU,8BA+RrB,CAAC"}
@@ -9,6 +9,7 @@
9
9
  import { Node, mergeAttributes } from "@tiptap/core";
10
10
  import { Plugin, PluginKey } from "@tiptap/pm/state";
11
11
  import { attachResize } from "../utils/resize";
12
+ import { applyCrossOriginEmbedAttrs, canEmbedCrossOrigin } from "../utils/embed";
12
13
  import { getEditorTranslator } from "../i18n";
13
14
  import { mediaRatioCss, normalizeMediaAlign, normalizeMediaHeight, normalizeMediaRatio } from "../utils/media-size";
14
15
  /** `1m30s` · `90` 처럼 적히는 유튜브 타임스탬프를 초로 바꾼다. */
@@ -126,24 +127,10 @@ export function toEmbedUrl(raw) {
126
127
  }
127
128
  return trimmed;
128
129
  }
129
- /**
130
- * 이 브라우저에서 **COEP 없는 남의 iframe** 을 실을 수 있는지.
131
- *
132
- * ⚠️ 정올은 전 브라우저에 `COEP: require-corp`(교차 출처 격리)를 건다. 격리된 문서에
133
- * 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부 해당)을 넣는 방법은
134
- * `credentialless` 속성뿐인데 **크로미움 전용**이다(파이어폭스 미구현·사파리 거부).
135
- * 그래서 정올 파폭·사파리에서는 mbus 영상이 지금 **빈 회색 박스**로 남아 있다.
136
- *
137
- * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
138
- * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
130
+ /*
131
+ * 판정은 `utils/embed.ts` 로 옮겼다 — `MbusVideo` 가 같은 규칙을 써야 하는데 여기
132
+ * 모듈 안에만 있어서 그쪽이 통째로 빠뜨리고 있었다(그 노드 주석 참조).
139
133
  */
140
- function canEmbedCrossOrigin() {
141
- if (typeof window === "undefined")
142
- return true;
143
- if (!window.crossOriginIsolated)
144
- return true;
145
- return "credentialless" in HTMLIFrameElement.prototype;
146
- }
147
134
  export const VideoEmbed = Node.create({
148
135
  name: "videoEmbed",
149
136
  group: "block",
@@ -261,11 +248,7 @@ export const VideoEmbed = Node.create({
261
248
  iframe.allow = "autoplay; fullscreen; encrypted-media; picture-in-picture";
262
249
  iframe.setAttribute("allowfullscreen", "");
263
250
  iframe.setAttribute("loading", "lazy");
264
- /*
265
- * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이
266
- * 속성 자체가 무시되므로 조건 없이 붙여도 해가 없다.
267
- */
268
- iframe.setAttribute("credentialless", "");
251
+ applyCrossOriginEmbedAttrs(iframe);
269
252
  iframe.style.cssText =
270
253
  "position:absolute;inset:0;width:100%;height:100%;border:0;display:block;";
271
254
  aspect.appendChild(iframe);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 「이 문서에서 **COEP 를 안 보내는 남의 iframe** 을 실을 수 있는가」.
3
+ *
4
+ * ⚠️ 정올은 전 문서에 `Cross-Origin-Embedder-Policy: require-corp`(교차 출처 격리)를
5
+ * 건다. 격리된 문서에 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부
6
+ * 해당)을 넣는 방법은 **`credentialless` 속성뿐**인데 크로미움 전용이다
7
+ * (파이어폭스 미구현·사파리 거부).
8
+ *
9
+ * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
10
+ * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
11
+ *
12
+ * ⚠️ 이 판정과 `applyCrossOriginEmbedAttrs` 는 **항상 짝으로** 쓴다. 한쪽만 쓰면
13
+ * 격리된 문서에서 브라우저가 iframe 을 막고 크롬이 제 오류 화면
14
+ * (`…에서 연결을 거부했습니다`)을 그 자리에 그린다 — `MbusVideo` 가 실제로 그랬다.
15
+ */
16
+ export declare function canEmbedCrossOrigin(): boolean;
17
+ /**
18
+ * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
19
+ * 무시되므로 조건 없이 붙여도 해가 없다.
20
+ */
21
+ export declare function applyCrossOriginEmbedAttrs(iframe: HTMLIFrameElement): void;
22
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/utils/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAI7C;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1E"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 「이 문서에서 **COEP 를 안 보내는 남의 iframe** 을 실을 수 있는가」.
3
+ *
4
+ * ⚠️ 정올은 전 문서에 `Cross-Origin-Embedder-Policy: require-corp`(교차 출처 격리)를
5
+ * 건다. 격리된 문서에 자기 COEP 를 안 보내는 제3자 iframe(유튜브·Vimeo·mbus 전부
6
+ * 해당)을 넣는 방법은 **`credentialless` 속성뿐**인데 크로미움 전용이다
7
+ * (파이어폭스 미구현·사파리 거부).
8
+ *
9
+ * 격리되지 않은 문서(코드패스의 비-크로미움 등)에서는 평범한 크로스오리진 iframe 이라
10
+ * 아무 문제가 없다 — 그래서 격리 여부와 지원 여부를 **함께** 본다.
11
+ *
12
+ * ⚠️ 이 판정과 `applyCrossOriginEmbedAttrs` 는 **항상 짝으로** 쓴다. 한쪽만 쓰면
13
+ * 격리된 문서에서 브라우저가 iframe 을 막고 크롬이 제 오류 화면
14
+ * (`…에서 연결을 거부했습니다`)을 그 자리에 그린다 — `MbusVideo` 가 실제로 그랬다.
15
+ */
16
+ export function canEmbedCrossOrigin() {
17
+ if (typeof window === "undefined")
18
+ return true;
19
+ if (!window.crossOriginIsolated)
20
+ return true;
21
+ return "credentialless" in HTMLIFrameElement.prototype;
22
+ }
23
+ /**
24
+ * 격리된 문서에서 COEP 없는 영상을 싣는 유일한 통로. 크로미움이 아니면 이 속성 자체가
25
+ * 무시되므로 조건 없이 붙여도 해가 없다.
26
+ */
27
+ export function applyCrossOriginEmbedAttrs(iframe) {
28
+ iframe.setAttribute("credentialless", "");
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",