@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 +96 -0
- package/dist/aeriz-wysiwyg.css +10 -1
- package/dist/aeriz-wysiwyg.esm.js +308 -39
- package/dist/aeriz-wysiwyg.esm.min.js +908 -692
- package/dist/aeriz-wysiwyg.js +308 -39
- package/dist/aeriz-wysiwyg.min.css +1 -1
- package/dist/aeriz-wysiwyg.min.js +17 -17
- package/dist-types/core/AerizEditor.d.ts.map +1 -1
- package/dist-types/core/blocks.d.ts +10 -0
- package/dist-types/core/blocks.d.ts.map +1 -1
- package/dist-types/core/import-html.d.ts +13 -2
- package/dist-types/core/import-html.d.ts.map +1 -1
- package/package.json +3 -2
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
|
package/dist/aeriz-wysiwyg.css
CHANGED
|
@@ -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
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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
|
|
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
|
|
2500
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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",
|