@aeriz/wysiwyg 1.0.0 → 1.0.1

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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @aeriz/wysiwyg
2
+
3
+ 한국어 친화 contentEditable WYSIWYG 에디터.
4
+
5
+ 자세한 동작 사양은 [SPEC.md](./SPEC.md) 를 참고하세요.
6
+
7
+ ## 설치
8
+
9
+ ### CDN — 스크립트 태그
10
+
11
+ 빌드 단계 없이 바로 사용. 라이브러리는 글로벌
12
+ `window.AerizWysiwyg` 와 단축 별칭 `window.AerizEditor` /
13
+ `window.AerizViewer` 로 노출됩니다.
14
+
15
+ ```html
16
+ <!-- ✓ 프로덕션 — 특정 버전 고정 (immutable, 가장 긴 캐시) -->
17
+ <link
18
+ rel="stylesheet"
19
+ href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@0.0.1/dist/aeriz-wysiwyg.min.css"
20
+ />
21
+ <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@0.0.1/dist/aeriz-wysiwyg.min.js"></script>
22
+
23
+ <!-- 1.x 트랙 — 패치만 자동 수신 -->
24
+ <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.1/dist/aeriz-wysiwyg.min.js"></script>
25
+
26
+ <!-- 항상 최신 — 프로토타이핑용 (프로덕션 권장 X) -->
27
+ <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@latest/dist/aeriz-wysiwyg.min.js"></script>
28
+
29
+ <!-- unpkg 미러 -->
30
+ <script src="https://unpkg.com/@aeriz/wysiwyg@0.0.1/dist/aeriz-wysiwyg.min.js"></script>
31
+
32
+ <div id="editor"></div>
33
+ <script>
34
+ const editor = new AerizEditor({
35
+ element: document.querySelector('#editor'),
36
+ initialHTML: '<p>안녕하세요</p>',
37
+ onChange: (html) => console.log(html),
38
+ });
39
+ </script>
40
+ ```
41
+
42
+ ### CDN — ES Module
43
+
44
+ ```html
45
+ <link
46
+ rel="stylesheet"
47
+ href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@0.0.1/dist/aeriz-wysiwyg.min.css"
48
+ />
49
+ <script type="module">
50
+ import {
51
+ AerizEditor,
52
+ AerizViewer,
53
+ } from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@0.0.1/dist/aeriz-wysiwyg.esm.js';
54
+
55
+ new AerizEditor({ element: document.querySelector('#editor') });
56
+ </script>
57
+ ```
58
+
59
+ ### NPM / Yarn / pnpm
60
+
61
+ 번들러(Vite, webpack, Rollup, esbuild, Parcel) 사용 프로젝트에서:
62
+
63
+ ```bash
64
+ npm install @aeriz/wysiwyg
65
+ # yarn add @aeriz/wysiwyg
66
+ # pnpm add @aeriz/wysiwyg
67
+ ```
68
+
69
+ ```ts
70
+ import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg';
71
+ import '@aeriz/wysiwyg/style.css';
72
+
73
+ const editor = new AerizEditor({
74
+ element: document.querySelector('#editor')!,
75
+ initialHTML: '<p>안녕하세요</p>',
76
+ theme: 'dark',
77
+ onChange: (html) => localStorage.setItem('draft', html),
78
+ });
79
+
80
+ const viewer = new AerizViewer({
81
+ element: document.querySelector('#preview')!,
82
+ initialHTML: localStorage.getItem('draft') ?? '',
83
+ });
84
+ ```
85
+
86
+ ## 빌드
87
+
88
+ ```bash
89
+ npm install
90
+ npm run build # dist/ 에 IIFE / ESM / CSS / .d.ts 생성
91
+ npm run dev # 데모 페이지 (http://localhost:5173)
92
+ ```
93
+
94
+ ## 라이선스
95
+
96
+ Apache-2.0
@@ -498,7 +498,7 @@
498
498
  aspect-ratio: 16 / 9;
499
499
  margin: 1em 0;
500
500
  border-radius: 8px;
501
- overflow: hidden;
501
+ /* overflow: hidden; */
502
502
  background: #000;
503
503
  border: 1px solid var(--aw-border);
504
504
  }
@@ -800,6 +800,15 @@
800
800
  transform: scale(1.15);
801
801
  }
802
802
 
803
+ /* 유튜브 임베드는 iframe 안에 자체 워터마크/컨트롤이 있어 코너에 핸들을 두면
804
+ * 클릭이 충돌한다. embed 안쪽 우하단에 약간 들여서 배치 — YouTube 자체 UI
805
+ * 영역과 약간 떨어진 위치. */
806
+
807
+ .aw-content > div.aw-youtube-embed > .aw-resize-handle {
808
+ right: 0px;
809
+ bottom: -15px;
810
+ }
811
+
803
812
  .aw-resizing {
804
813
  user-select: none;
805
814
  }
@@ -2386,10 +2386,124 @@ function closeAllPopovers() {
2386
2386
  function setLocale(locale) {
2387
2387
  }
2388
2388
  const STRIP_TAGS = ["script", "noscript", "meta", "link", "style", "base"];
2389
+ const INHERITED_PROPERTIES = /* @__PURE__ */ new Set([
2390
+ // CSS 2.1
2391
+ "border-collapse",
2392
+ "border-spacing",
2393
+ "caption-side",
2394
+ "color",
2395
+ "cursor",
2396
+ "direction",
2397
+ "empty-cells",
2398
+ "font-family",
2399
+ "font-size",
2400
+ "font-style",
2401
+ "font-variant",
2402
+ "font-weight",
2403
+ "letter-spacing",
2404
+ "line-height",
2405
+ "list-style",
2406
+ "list-style-image",
2407
+ "list-style-position",
2408
+ "list-style-type",
2409
+ "orphans",
2410
+ "quotes",
2411
+ "text-align",
2412
+ "text-indent",
2413
+ "text-transform",
2414
+ "visibility",
2415
+ "white-space",
2416
+ "widows",
2417
+ "word-spacing",
2418
+ // CSS 3+
2419
+ "caret-color",
2420
+ "font",
2421
+ "font-feature-settings",
2422
+ "font-kerning",
2423
+ "font-language-override",
2424
+ "font-optical-sizing",
2425
+ "font-palette",
2426
+ "font-size-adjust",
2427
+ "font-stretch",
2428
+ "font-synthesis",
2429
+ "font-synthesis-weight",
2430
+ "font-synthesis-style",
2431
+ "font-synthesis-small-caps",
2432
+ "font-variant-alternates",
2433
+ "font-variant-caps",
2434
+ "font-variant-east-asian",
2435
+ "font-variant-emoji",
2436
+ "font-variant-ligatures",
2437
+ "font-variant-numeric",
2438
+ "font-variant-position",
2439
+ "font-variation-settings",
2440
+ "hanging-punctuation",
2441
+ "hyphens",
2442
+ "image-rendering",
2443
+ "line-break",
2444
+ "overflow-wrap",
2445
+ "pointer-events",
2446
+ "ruby-align",
2447
+ "ruby-position",
2448
+ "tab-size",
2449
+ "text-align-last",
2450
+ "text-combine-upright",
2451
+ "text-decoration-color",
2452
+ "text-emphasis",
2453
+ "text-emphasis-color",
2454
+ "text-emphasis-position",
2455
+ "text-emphasis-style",
2456
+ "text-justify",
2457
+ "text-orientation",
2458
+ "text-rendering",
2459
+ "text-shadow",
2460
+ "text-size-adjust",
2461
+ "text-underline-offset",
2462
+ "text-underline-position",
2463
+ "white-space-collapse",
2464
+ "word-break",
2465
+ "word-wrap",
2466
+ "writing-mode",
2467
+ "color-scheme",
2468
+ "print-color-adjust",
2469
+ // Vendor
2470
+ "-webkit-font-smoothing",
2471
+ "-moz-osx-font-smoothing",
2472
+ "-webkit-locale",
2473
+ "-webkit-text-fill-color",
2474
+ "-webkit-text-stroke",
2475
+ "-webkit-text-stroke-color",
2476
+ "-webkit-text-stroke-width",
2477
+ "-webkit-print-color-adjust",
2478
+ // SVG
2479
+ "clip-rule",
2480
+ "color-interpolation",
2481
+ "color-interpolation-filters",
2482
+ "color-rendering",
2483
+ "fill",
2484
+ "fill-opacity",
2485
+ "fill-rule",
2486
+ "marker",
2487
+ "marker-end",
2488
+ "marker-mid",
2489
+ "marker-start",
2490
+ "paint-order",
2491
+ "shape-rendering",
2492
+ "stroke",
2493
+ "stroke-dasharray",
2494
+ "stroke-dashoffset",
2495
+ "stroke-linecap",
2496
+ "stroke-linejoin",
2497
+ "stroke-miterlimit",
2498
+ "stroke-opacity",
2499
+ "stroke-width",
2500
+ "text-anchor"
2501
+ ]);
2389
2502
  async function importHtmlWithComputedStyles(html, options = {}) {
2390
2503
  const settleMs = options.settleMs ?? 1e3;
2391
2504
  const timeoutMs = options.timeoutMs ?? 3e4;
2392
2505
  const width = options.width ?? 1024;
2506
+ const mountTarget = options.mountTarget ?? null;
2393
2507
  return new Promise((resolve, reject) => {
2394
2508
  const iframe = document.createElement("iframe");
2395
2509
  iframe.setAttribute("aria-hidden", "true");
@@ -2442,7 +2556,7 @@ async function importHtmlWithComputedStyles(html, options = {}) {
2442
2556
  fail(new Error("iframe document 접근 실패"));
2443
2557
  return;
2444
2558
  }
2445
- const baked = bakeBodyToImportable(doc.body, w);
2559
+ const baked = bakeBodyToImportable(doc.body, w, mountTarget);
2446
2560
  window.clearTimeout(timer);
2447
2561
  finish(baked);
2448
2562
  } catch (err) {
@@ -2459,23 +2573,30 @@ async function importHtmlWithComputedStyles(html, options = {}) {
2459
2573
  document.body.appendChild(iframe);
2460
2574
  });
2461
2575
  }
2462
- function bakeBodyToImportable(body, win) {
2576
+ function bakeBodyToImportable(body, win, mountTarget) {
2463
2577
  var _a;
2578
+ const doc = body.ownerDocument;
2579
+ if (!doc) throw new Error("body has no ownerDocument");
2464
2580
  const wrapper = document.createElement("div");
2465
- for (let i = 0; i < body.attributes.length; i++) {
2466
- const attr = body.attributes.item(i);
2467
- if (!attr) continue;
2468
- if (attr.name === "style") continue;
2469
- wrapper.setAttribute(attr.name, attr.value);
2470
- }
2471
- const bodyStyle = computedStyleToInline(body, win);
2472
- if (bodyStyle.length > 0) {
2473
- wrapper.setAttribute("style", bodyStyle);
2474
- }
2475
- for (const child of Array.from(body.children)) {
2476
- const imported = document.importNode(child, true);
2477
- applyComputedStylesParallel(child, imported, win);
2478
- wrapper.appendChild(imported);
2581
+ const baseline = createBaselineProbe(doc, win, mountTarget);
2582
+ try {
2583
+ for (let i = 0; i < body.attributes.length; i++) {
2584
+ const attr = body.attributes.item(i);
2585
+ if (!attr) continue;
2586
+ if (attr.name === "style") continue;
2587
+ wrapper.setAttribute(attr.name, attr.value);
2588
+ }
2589
+ const wrapperStyle = computeMinimalInline(body, null, "div", baseline.get, win);
2590
+ if (wrapperStyle.length > 0) {
2591
+ wrapper.setAttribute("style", wrapperStyle);
2592
+ }
2593
+ for (const child of Array.from(body.children)) {
2594
+ const imported = document.importNode(child, true);
2595
+ applyComputedStylesParallel(child, imported, body, baseline.get, win);
2596
+ wrapper.appendChild(imported);
2597
+ }
2598
+ } finally {
2599
+ baseline.cleanup();
2479
2600
  }
2480
2601
  for (const tag of STRIP_TAGS) {
2481
2602
  for (const el of Array.from(wrapper.querySelectorAll(tag))) {
@@ -2489,24 +2610,83 @@ function bakeBodyToImportable(body, win) {
2489
2610
  wrapper.setAttribute("data-imported-html", "true");
2490
2611
  return [wrapper];
2491
2612
  }
2492
- function computedStyleToInline(el, win) {
2613
+ function createBaselineProbe(iframeDoc, iframeWin, mountTarget) {
2614
+ var _a;
2615
+ let probeDoc;
2616
+ let probeWin;
2617
+ let probeHost;
2618
+ const targetWin = ((_a = mountTarget == null ? void 0 : mountTarget.ownerDocument) == null ? void 0 : _a.defaultView) ?? null;
2619
+ if (mountTarget && targetWin) {
2620
+ probeDoc = mountTarget.ownerDocument;
2621
+ probeWin = targetWin;
2622
+ const host = probeDoc.createElement("div");
2623
+ host.setAttribute("data-imported-html", "true");
2624
+ host.setAttribute("aria-hidden", "true");
2625
+ host.style.cssText = "position:absolute;left:-99999px;top:0;visibility:hidden;pointer-events:none;";
2626
+ mountTarget.appendChild(host);
2627
+ probeHost = host;
2628
+ } else {
2629
+ probeDoc = iframeDoc;
2630
+ probeWin = iframeWin;
2631
+ const host = probeDoc.createElement("div");
2632
+ host.setAttribute("aria-hidden", "true");
2633
+ host.style.cssText = "position:absolute;left:-99999px;top:0;width:0;height:0;overflow:hidden;visibility:hidden;";
2634
+ probeDoc.body.appendChild(host);
2635
+ probeHost = host;
2636
+ }
2637
+ const cache = /* @__PURE__ */ new Map();
2638
+ const get = (tagName) => {
2639
+ const cached = cache.get(tagName);
2640
+ if (cached) return cached;
2641
+ const probe = probeDoc.createElement(tagName);
2642
+ probeHost.appendChild(probe);
2643
+ const cs = probeWin.getComputedStyle(probe);
2644
+ const map = /* @__PURE__ */ new Map();
2645
+ for (let i = 0; i < cs.length; i++) {
2646
+ const prop = cs.item(i);
2647
+ if (prop) map.set(prop, cs.getPropertyValue(prop));
2648
+ }
2649
+ probeHost.removeChild(probe);
2650
+ cache.set(tagName, map);
2651
+ return map;
2652
+ };
2653
+ const cleanup = () => {
2654
+ if (probeHost.parentNode) probeHost.parentNode.removeChild(probeHost);
2655
+ };
2656
+ return { get, cleanup };
2657
+ }
2658
+ function computeMinimalInline(el, parent, baselineTagName, getBaseline, win) {
2493
2659
  const cs = win.getComputedStyle(el);
2660
+ const parentCs = parent ? win.getComputedStyle(parent) : null;
2661
+ const baseline = getBaseline(baselineTagName);
2494
2662
  let cssText = "";
2495
2663
  for (let i = 0; i < cs.length; i++) {
2496
2664
  const prop = cs.item(i);
2497
2665
  if (!prop) continue;
2498
2666
  const val = cs.getPropertyValue(prop);
2499
- if (val && val.length > 0) {
2500
- cssText += `${prop}:${val};`;
2667
+ if (!val) continue;
2668
+ if (INHERITED_PROPERTIES.has(prop)) {
2669
+ if (parentCs && parentCs.getPropertyValue(prop) === val) continue;
2670
+ } else {
2671
+ if (baseline.get(prop) === val) continue;
2501
2672
  }
2673
+ cssText += `${prop}:${val};`;
2502
2674
  }
2503
2675
  return cssText;
2504
2676
  }
2505
- function applyComputedStylesParallel(orig, copy, win) {
2677
+ function applyComputedStylesParallel(orig, copy, origParent, getBaseline, win) {
2506
2678
  if (orig.nodeType === 1 && copy.nodeType === 1) {
2507
- const cssText = computedStyleToInline(orig, win);
2679
+ const cssText = computeMinimalInline(
2680
+ orig,
2681
+ origParent,
2682
+ orig.tagName.toLowerCase(),
2683
+ getBaseline,
2684
+ win
2685
+ );
2508
2686
  if (cssText.length > 0) {
2509
2687
  copy.setAttribute("style", cssText);
2688
+ } else if (copy.hasAttribute("style")) {
2689
+ copy.removeAttribute("style");
2510
2690
  }
2511
2691
  }
2512
2692
  const origChildren = orig.children;
@@ -2515,7 +2695,7 @@ function applyComputedStylesParallel(orig, copy, win) {
2515
2695
  for (let i = 0; i < n; i++) {
2516
2696
  const oc = origChildren[i];
2517
2697
  const cc = copyChildren[i];
2518
- if (oc && cc) applyComputedStylesParallel(oc, cc, win);
2698
+ if (oc && cc) applyComputedStylesParallel(oc, cc, orig, getBaseline, win);
2519
2699
  }
2520
2700
  }
2521
2701
  function markdownToBlocks(md) {
@@ -2732,6 +2912,8 @@ const PARAGRAPH = {
2732
2912
  label: "단락",
2733
2913
  allowsInlineFormat: true,
2734
2914
  allowedMarks: ALL_INLINE_MARKS,
2915
+ allowsColor: true,
2916
+ allowsFontSize: true,
2735
2917
  matches: (el) => el.tagName === "P" ? { kind: "paragraph", attrs: {} } : null,
2736
2918
  create: () => fillEmpty(document.createElement("p")),
2737
2919
  isEmpty: (el) => isElementBlank(el)
@@ -2743,6 +2925,10 @@ const HEADING = {
2743
2925
  // 사용자 지정 — 제목은 italic 만 허용. (bold 는 heading 자체가 굵으므로
2744
2926
  // 중복, underline / strike 는 가독성/시맨틱 면에서 부적합.)
2745
2927
  allowedMarks: ["italic"],
2928
+ // 글자색/배경색은 시맨틱 헤딩 톤 보존을 위해 X.
2929
+ allowsColor: false,
2930
+ // 글자 크기는 heading level (h1~h6) 로 결정되므로 X.
2931
+ allowsFontSize: false,
2746
2932
  matches: (el) => {
2747
2933
  const m = /^H([1-6])$/.exec(el.tagName);
2748
2934
  if (!m) return null;
@@ -2759,6 +2945,8 @@ const BULLET_LIST = {
2759
2945
  label: "글머리 목록",
2760
2946
  allowsInlineFormat: true,
2761
2947
  allowedMarks: ALL_INLINE_MARKS,
2948
+ allowsColor: true,
2949
+ allowsFontSize: true,
2762
2950
  matches: (el) => el.tagName === "UL" ? { kind: "bulletList", attrs: {} } : null,
2763
2951
  create: () => createListElement("ul"),
2764
2952
  isEmpty: (el) => isListEmpty(el)
@@ -2768,6 +2956,8 @@ const ORDERED_LIST = {
2768
2956
  label: "번호 매기기 목록",
2769
2957
  allowsInlineFormat: true,
2770
2958
  allowedMarks: ALL_INLINE_MARKS,
2959
+ allowsColor: true,
2960
+ allowsFontSize: true,
2771
2961
  matches: (el) => el.tagName === "OL" ? { kind: "orderedList", attrs: {} } : null,
2772
2962
  create: () => createListElement("ol"),
2773
2963
  isEmpty: (el) => isListEmpty(el)
@@ -2779,6 +2969,9 @@ const BLOCKQUOTE = {
2779
2969
  // 사용자 지정 — 인용구는 italic 제외(인용 자체가 이미 italic 으로 렌더되어
2780
2970
  // 중복). bold / underline / strike 만 허용.
2781
2971
  allowedMarks: ["bold", "underline", "strike"],
2972
+ // 인용구 자체의 톤(이탤릭/주변 색조) 을 보존.
2973
+ allowsColor: false,
2974
+ allowsFontSize: true,
2782
2975
  matches: (el) => el.tagName === "BLOCKQUOTE" ? { kind: "blockquote", attrs: {} } : null,
2783
2976
  create: () => {
2784
2977
  const bq = document.createElement("blockquote");
@@ -2792,6 +2985,10 @@ const CODE_BLOCK = {
2792
2985
  label: "코드 블록",
2793
2986
  allowsInlineFormat: false,
2794
2987
  allowedMarks: [],
2988
+ // 코드 블록은 인라인 서식은 막혀 있지만 글자 크기 조정은 (가독성) 의미가
2989
+ // 있어 예외적으로 허용. 색은 syntax highlight 와 충돌하므로 X.
2990
+ allowsColor: false,
2991
+ allowsFontSize: true,
2795
2992
  matches: (el) => {
2796
2993
  if (el.tagName !== "PRE") return null;
2797
2994
  const code = el.querySelector(":scope > code");
@@ -2812,6 +3009,8 @@ const IMAGE = {
2812
3009
  label: "이미지",
2813
3010
  allowsInlineFormat: false,
2814
3011
  allowedMarks: [],
3012
+ allowsColor: false,
3013
+ allowsFontSize: false,
2815
3014
  matches: (el) => {
2816
3015
  if (el.tagName === "FIGURE") {
2817
3016
  const img = el.querySelector(":scope > img");
@@ -2875,6 +3074,8 @@ const TABLE = {
2875
3074
  label: "표",
2876
3075
  allowsInlineFormat: true,
2877
3076
  allowedMarks: ALL_INLINE_MARKS,
3077
+ allowsColor: true,
3078
+ allowsFontSize: true,
2878
3079
  matches: (el) => {
2879
3080
  var _a;
2880
3081
  if (el.tagName !== "TABLE") return null;
@@ -2907,6 +3108,8 @@ const HR = {
2907
3108
  label: "구분선",
2908
3109
  allowsInlineFormat: false,
2909
3110
  allowedMarks: [],
3111
+ allowsColor: false,
3112
+ allowsFontSize: false,
2910
3113
  matches: (el) => el.tagName === "HR" ? { kind: "hr", attrs: {} } : null,
2911
3114
  create: () => {
2912
3115
  const hr = document.createElement("hr");
@@ -2920,6 +3123,8 @@ const YOUTUBE = {
2920
3123
  label: "유튜브",
2921
3124
  allowsInlineFormat: false,
2922
3125
  allowedMarks: [],
3126
+ allowsColor: false,
3127
+ allowsFontSize: false,
2923
3128
  matches: (el) => {
2924
3129
  if (el.tagName !== "DIV") return null;
2925
3130
  if (!el.classList.contains("aw-youtube-embed")) return null;
@@ -2960,6 +3165,8 @@ const LINK_CARD = {
2960
3165
  label: "링크",
2961
3166
  allowsInlineFormat: false,
2962
3167
  allowedMarks: [],
3168
+ allowsColor: false,
3169
+ allowsFontSize: false,
2963
3170
  matches: (el) => {
2964
3171
  var _a, _b, _c, _d, _e;
2965
3172
  if (el.tagName !== "A") return null;
@@ -3047,6 +3254,8 @@ const FILE = {
3047
3254
  label: "파일",
3048
3255
  allowsInlineFormat: false,
3049
3256
  allowedMarks: [],
3257
+ allowsColor: false,
3258
+ allowsFontSize: false,
3050
3259
  matches: (el) => {
3051
3260
  if (el.tagName !== "A") return null;
3052
3261
  if (!el.classList.contains("aw-file-attachment")) return null;
@@ -3130,6 +3339,8 @@ const IMPORTED_HTML = {
3130
3339
  label: "HTML",
3131
3340
  allowsInlineFormat: false,
3132
3341
  allowedMarks: [],
3342
+ allowsColor: false,
3343
+ allowsFontSize: false,
3133
3344
  matches: (el) => {
3134
3345
  if (el.tagName !== "DIV") return null;
3135
3346
  if (el.getAttribute("data-imported-html") !== "true") return null;
@@ -3154,6 +3365,8 @@ const UNKNOWN = {
3154
3365
  label: "알 수 없음",
3155
3366
  allowsInlineFormat: false,
3156
3367
  allowedMarks: [],
3368
+ allowsColor: false,
3369
+ allowsFontSize: false,
3157
3370
  matches: () => null,
3158
3371
  create: () => fillEmpty(document.createElement("p")),
3159
3372
  isEmpty: (el) => isElementBlank(el)
@@ -3428,6 +3641,72 @@ function fillInlineContent(newEl, kind, content) {
3428
3641
  if (!target) return;
3429
3642
  while (target.firstChild) target.removeChild(target.firstChild);
3430
3643
  for (const node of content) target.appendChild(node);
3644
+ const type = BLOCK_TYPE_BY_KIND[kind];
3645
+ if (type) sanitizeInlineFormat(target, type);
3646
+ }
3647
+ function sanitizeInlineFormat(container, type) {
3648
+ const allowed = new Set(type.allowedMarks);
3649
+ for (const el of Array.from(container.querySelectorAll("*"))) {
3650
+ if (!(el instanceof HTMLElement)) continue;
3651
+ if (!el.parentNode) continue;
3652
+ const tag = el.tagName.toLowerCase();
3653
+ if ((tag === "b" || tag === "strong") && !allowed.has("bold") || (tag === "i" || tag === "em") && !allowed.has("italic") || tag === "u" && !allowed.has("underline") || (tag === "s" || tag === "strike" || tag === "del") && !allowed.has("strike")) {
3654
+ unwrapElement(el);
3655
+ continue;
3656
+ }
3657
+ if (tag === "span" || tag === "font") {
3658
+ const s = el.style;
3659
+ if (!allowed.has("bold")) {
3660
+ const fw = s.fontWeight;
3661
+ if (fw === "bold" || fw === "bolder" || /^[7-9]\d{2}$|^1000$/.test(fw)) {
3662
+ s.removeProperty("font-weight");
3663
+ }
3664
+ }
3665
+ if (!allowed.has("italic") && s.fontStyle === "italic") {
3666
+ s.removeProperty("font-style");
3667
+ }
3668
+ const td = s.textDecorationLine || s.textDecoration || "";
3669
+ if (td) {
3670
+ const parts = td.split(/\s+/);
3671
+ const kept = parts.filter((p) => {
3672
+ if (p === "underline" && !allowed.has("underline")) return false;
3673
+ if (p === "line-through" && !allowed.has("strike")) return false;
3674
+ return true;
3675
+ });
3676
+ if (kept.length !== parts.length) {
3677
+ s.removeProperty("text-decoration");
3678
+ s.removeProperty("text-decoration-line");
3679
+ if (kept.length > 0) s.textDecoration = kept.join(" ");
3680
+ }
3681
+ }
3682
+ if (!type.allowsColor) {
3683
+ s.removeProperty("color");
3684
+ s.removeProperty("background-color");
3685
+ if (tag === "font") el.removeAttribute("color");
3686
+ }
3687
+ if (!type.allowsFontSize) {
3688
+ s.removeProperty("font-size");
3689
+ if (tag === "font") el.removeAttribute("size");
3690
+ }
3691
+ if (!type.allowsInlineFormat) {
3692
+ s.removeProperty("font-family");
3693
+ if (tag === "font") el.removeAttribute("face");
3694
+ }
3695
+ const hasStyle = s.cssText.trim().length > 0;
3696
+ const hasOtherAttrs = Array.from(el.attributes).some(
3697
+ (a) => a.name !== "style"
3698
+ );
3699
+ if (!hasStyle && !hasOtherAttrs) {
3700
+ unwrapElement(el);
3701
+ }
3702
+ }
3703
+ }
3704
+ }
3705
+ function unwrapElement(el) {
3706
+ const parent = el.parentNode;
3707
+ if (!parent) return;
3708
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
3709
+ parent.removeChild(el);
3431
3710
  }
3432
3711
  function resolveTheme(theme = {}) {
3433
3712
  const fg = theme.fg || "currentColor";
@@ -4750,7 +5029,9 @@ class AerizEditor {
4750
5029
  }
4751
5030
  let imported;
4752
5031
  try {
4753
- imported = await importHtmlWithComputedStyles(result.html);
5032
+ imported = await importHtmlWithComputedStyles(result.html, {
5033
+ mountTarget: this.content
5034
+ });
4754
5035
  } catch (err) {
4755
5036
  console.error("HTML 가져오기 실패:", err);
4756
5037
  const msg = err instanceof Error ? err.message : String(err);
@@ -5267,27 +5548,15 @@ function isInlineFormatAllowed(active) {
5267
5548
  const type = BLOCK_TYPE_BY_KIND[active.kind] ?? BLOCK_TYPE_BY_KIND.unknown;
5268
5549
  return type.allowsInlineFormat;
5269
5550
  }
5270
- const FONT_SIZE_ALLOWED_KINDS = /* @__PURE__ */ new Set([
5271
- "paragraph",
5272
- "bulletList",
5273
- "orderedList",
5274
- "blockquote",
5275
- "codeBlock",
5276
- "table"
5277
- ]);
5278
5551
  function isFontSizeAllowed(active) {
5279
5552
  if (!active) return false;
5280
- return FONT_SIZE_ALLOWED_KINDS.has(active.kind);
5553
+ const type = BLOCK_TYPE_BY_KIND[active.kind] ?? BLOCK_TYPE_BY_KIND.unknown;
5554
+ return type.allowsFontSize;
5281
5555
  }
5282
- const COLOR_ALLOWED_KINDS = /* @__PURE__ */ new Set([
5283
- "paragraph",
5284
- "bulletList",
5285
- "orderedList",
5286
- "table"
5287
- ]);
5288
5556
  function isColorAllowed(active) {
5289
5557
  if (!active) return false;
5290
- return COLOR_ALLOWED_KINDS.has(active.kind);
5558
+ const type = BLOCK_TYPE_BY_KIND[active.kind] ?? BLOCK_TYPE_BY_KIND.unknown;
5559
+ return type.allowsColor;
5291
5560
  }
5292
5561
  const ALIGN_ALLOWED_KINDS = /* @__PURE__ */ new Set([
5293
5562
  "paragraph",