@aeriz/wysiwyg 1.0.5 → 2.0.0

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.
Files changed (129) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +82 -573
  3. package/angular/README.md +10 -4
  4. package/angular/aeriz-editor.directive.ts +40 -18
  5. package/dist/adapters/jquery.cjs +1 -1
  6. package/dist/adapters/jquery.js +14 -14
  7. package/dist/adapters/shared.cjs +1 -1
  8. package/dist/adapters/shared.js +59 -56
  9. package/dist/adapters/vue.cjs +1 -1
  10. package/dist/adapters/vue.js +64 -56
  11. package/dist/adapters/vue2.cjs +1 -1
  12. package/dist/adapters/vue2.js +24 -17
  13. package/dist/adapters/wc.cjs +1 -1
  14. package/dist/adapters/wc.js +108 -72
  15. package/dist/aeriz-wysiwyg-adapter.js +24 -32
  16. package/dist/aeriz-wysiwyg-adapter.min.js +1 -1
  17. package/dist/aeriz-wysiwyg-highlight.js +964 -0
  18. package/dist/aeriz-wysiwyg-highlight.min.js +5 -0
  19. package/dist/aeriz-wysiwyg-jquery.js +25 -32
  20. package/dist/aeriz-wysiwyg-jquery.min.js +1 -1
  21. package/dist/aeriz-wysiwyg-wc.js +80 -43
  22. package/dist/aeriz-wysiwyg-wc.min.js +1 -1
  23. package/dist/aeriz-wysiwyg.cjs +3525 -1744
  24. package/dist/aeriz-wysiwyg.css +2104 -1904
  25. package/dist/aeriz-wysiwyg.esm.js +3525 -1744
  26. package/dist/aeriz-wysiwyg.esm.min.js +4840 -3581
  27. package/dist/aeriz-wysiwyg.js +3525 -1744
  28. package/dist/aeriz-wysiwyg.min.cjs +51 -46
  29. package/dist/aeriz-wysiwyg.min.css +1 -1
  30. package/dist/aeriz-wysiwyg.min.js +51 -46
  31. package/dist/highlight.cjs +5 -0
  32. package/dist/highlight.js +895 -0
  33. package/dist/server.cjs +24 -19
  34. package/dist/server.js +612 -251
  35. package/dist-types/adapters/shared.d.ts +0 -8
  36. package/dist-types/adapters/shared.d.ts.map +1 -1
  37. package/dist-types/adapters/vue.d.ts +62 -21
  38. package/dist-types/adapters/vue.d.ts.map +1 -1
  39. package/dist-types/adapters/vue2.d.ts +33 -14
  40. package/dist-types/adapters/vue2.d.ts.map +1 -1
  41. package/dist-types/adapters/wc.d.ts +15 -6
  42. package/dist-types/adapters/wc.d.ts.map +1 -1
  43. package/dist-types/cjs/adapters/shared.d.cts +0 -8
  44. package/dist-types/cjs/adapters/vue.d.cts +62 -21
  45. package/dist-types/cjs/adapters/vue2.d.cts +33 -14
  46. package/dist-types/cjs/adapters/wc.d.cts +15 -6
  47. package/dist-types/cjs/core/AerizEditor.d.cts +204 -48
  48. package/dist-types/cjs/core/AerizViewer.d.cts +7 -0
  49. package/dist-types/cjs/core/block-meta.d.cts +40 -0
  50. package/dist-types/cjs/core/block-tracker.d.cts +64 -0
  51. package/dist-types/cjs/core/blocks.d.cts +39 -4
  52. package/dist-types/cjs/core/fonts.d.cts +33 -46
  53. package/dist-types/cjs/core/history.d.cts +28 -10
  54. package/dist-types/cjs/core/html-to-blocks.d.cts +10 -0
  55. package/dist-types/cjs/core/i18n.d.cts +44 -0
  56. package/dist-types/cjs/core/import-html.d.cts +0 -13
  57. package/dist-types/cjs/core/insert-fragment.d.cts +21 -0
  58. package/dist-types/cjs/core/paste-clean.d.cts +36 -0
  59. package/dist-types/cjs/core/sanitize-dom.d.cts +21 -0
  60. package/dist-types/cjs/core/sanitize-html.d.cts +8 -0
  61. package/dist-types/cjs/core/sanitize-policy.d.cts +22 -0
  62. package/dist-types/cjs/core/selection.d.cts +5 -0
  63. package/dist-types/cjs/core/table-grid.d.cts +87 -0
  64. package/dist-types/cjs/core/table-ops.d.cts +2 -22
  65. package/dist-types/cjs/highlight/index.d.cts +5 -0
  66. package/dist-types/cjs/index.d.cts +19 -11
  67. package/dist-types/cjs/server/index.d.cts +4 -5
  68. package/dist-types/cjs/types/index.d.cts +63 -15
  69. package/dist-types/cjs/ui/Toolbar.d.cts +10 -0
  70. package/dist-types/cjs/ui/doc-tools.d.cts +58 -0
  71. package/dist-types/cjs/ui/popovers.d.cts +14 -25
  72. package/dist-types/core/AerizEditor.d.ts +204 -48
  73. package/dist-types/core/AerizEditor.d.ts.map +1 -1
  74. package/dist-types/core/AerizViewer.d.ts +7 -0
  75. package/dist-types/core/AerizViewer.d.ts.map +1 -1
  76. package/dist-types/core/block-meta.d.ts +40 -0
  77. package/dist-types/core/block-meta.d.ts.map +1 -1
  78. package/dist-types/core/block-policy.d.ts.map +1 -1
  79. package/dist-types/core/block-tracker.d.ts +64 -0
  80. package/dist-types/core/block-tracker.d.ts.map +1 -0
  81. package/dist-types/core/blocks.d.ts +39 -4
  82. package/dist-types/core/blocks.d.ts.map +1 -1
  83. package/dist-types/core/fonts.d.ts +33 -46
  84. package/dist-types/core/fonts.d.ts.map +1 -1
  85. package/dist-types/core/history.d.ts +28 -10
  86. package/dist-types/core/history.d.ts.map +1 -1
  87. package/dist-types/core/html-mini.d.ts.map +1 -1
  88. package/dist-types/core/html-to-blocks.d.ts +10 -0
  89. package/dist-types/core/html-to-blocks.d.ts.map +1 -0
  90. package/dist-types/core/i18n.d.ts +44 -0
  91. package/dist-types/core/i18n.d.ts.map +1 -1
  92. package/dist-types/core/import-html.d.ts +0 -13
  93. package/dist-types/core/import-html.d.ts.map +1 -1
  94. package/dist-types/core/import-markdown.d.ts.map +1 -1
  95. package/dist-types/core/insert-fragment.d.ts +21 -0
  96. package/dist-types/core/insert-fragment.d.ts.map +1 -0
  97. package/dist-types/core/markdown-export.d.ts.map +1 -1
  98. package/dist-types/core/paste-clean.d.ts +36 -0
  99. package/dist-types/core/paste-clean.d.ts.map +1 -0
  100. package/dist-types/core/render-string.d.ts.map +1 -1
  101. package/dist-types/core/sanitize-dom.d.ts +21 -0
  102. package/dist-types/core/sanitize-dom.d.ts.map +1 -0
  103. package/dist-types/core/sanitize-html.d.ts +8 -0
  104. package/dist-types/core/sanitize-html.d.ts.map +1 -0
  105. package/dist-types/core/sanitize-policy.d.ts +22 -0
  106. package/dist-types/core/sanitize-policy.d.ts.map +1 -0
  107. package/dist-types/core/selection.d.ts +5 -0
  108. package/dist-types/core/selection.d.ts.map +1 -1
  109. package/dist-types/core/snapshot.d.ts.map +1 -1
  110. package/dist-types/core/table-grid.d.ts +87 -0
  111. package/dist-types/core/table-grid.d.ts.map +1 -0
  112. package/dist-types/core/table-ops.d.ts +2 -22
  113. package/dist-types/core/table-ops.d.ts.map +1 -1
  114. package/dist-types/highlight/index.d.ts +5 -0
  115. package/dist-types/highlight/index.d.ts.map +1 -0
  116. package/dist-types/index.d.ts +19 -11
  117. package/dist-types/index.d.ts.map +1 -1
  118. package/dist-types/server/index.d.ts +4 -5
  119. package/dist-types/server/index.d.ts.map +1 -1
  120. package/dist-types/types/index.d.ts +63 -15
  121. package/dist-types/types/index.d.ts.map +1 -1
  122. package/dist-types/ui/Toolbar.d.ts +10 -0
  123. package/dist-types/ui/Toolbar.d.ts.map +1 -1
  124. package/dist-types/ui/doc-tools.d.ts +58 -0
  125. package/dist-types/ui/doc-tools.d.ts.map +1 -0
  126. package/dist-types/ui/icons.d.ts.map +1 -1
  127. package/dist-types/ui/popovers.d.ts +14 -25
  128. package/dist-types/ui/popovers.d.ts.map +1 -1
  129. package/package.json +70 -5
package/README.md CHANGED
@@ -1,634 +1,143 @@
1
1
  # @aeriz/wysiwyg
2
2
 
3
- 한국어 친화 contentEditable WYSIWYG 에디터.
3
+ A WYSIWYG editor, light as air, built for Korean documents. It handles merged-cell tables, clean pastes from Word, Hangul (HWP) and Excel, pt font sizes and Korean fonts, line height and letter spacing, find and replace, source view and print. It has zero runtime dependencies.
4
4
 
5
- 자세한 동작 사양은 [SPEC.md](./SPEC.md) 를 참고하세요.
6
-
7
- ## 지원 환경
8
-
9
- | 대상 | 진입점 | 방식 |
10
- | --- | --- | --- |
11
- | Vanilla JS | `@aeriz/wysiwyg` | 클래스 직접 사용 |
12
- | Web Component | `@aeriz/wysiwyg/wc` | `<aeriz-editor>` 커스텀 엘리먼트 |
13
- | jQuery | `@aeriz/wysiwyg/jquery` | `$.fn.aerizEditor` 플러그인 |
14
- | React 16.8+ / 17 / 18 / 19 | `@aeriz/wysiwyg/react` | `<AerizEditor />` 컴포넌트 |
15
- | Vue 3 | `@aeriz/wysiwyg/vue` | `v-model` 컴포넌트 |
16
- | Vue 2.6 / 2.7 | `@aeriz/wysiwyg/vue2` | `v-model` 컴포넌트 |
17
- | Svelte 3 / 4 / 5 | `@aeriz/wysiwyg/svelte` | `use:aerizEditor` 액션 |
18
- | Angular 14+ | `@aeriz/wysiwyg/angular` | `[aerizEditor]` 디렉티브 (`ngModel` 지원) |
19
- | Node / Deno / Bun / Cloudflare Workers | `@aeriz/wysiwyg/server` | DOM 없는 렌더러 · 변환기 |
20
-
21
- 빌드 도구가 없는 페이지도 지원합니다 — 코어뿐 아니라 **어댑터도 스크립트
22
- 태그로 로드**할 수 있습니다 ([CDN 절](#cdn--어댑터도-스크립트-태그로) 참고).
23
-
24
- 프레임워크 패키지는 모두 **optional peerDependency** 입니다. React 프로젝트에
25
- Vue 가 설치될 일은 없습니다.
26
-
27
- ## 설치
5
+ **Docs and live demo: https://aeriz.dev/wysiwyg/**
28
6
 
29
7
  ```bash
30
8
  npm install @aeriz/wysiwyg
31
- # yarn add @aeriz/wysiwyg
32
- # pnpm add @aeriz/wysiwyg
33
9
  ```
34
10
 
35
- 스타일시트는 어느 프레임워크를 쓰든 한 번 로드해야 합니다.
36
-
37
- ```ts
38
- import '@aeriz/wysiwyg/style.css';
39
- ```
40
-
41
- ---
42
-
43
- ## Vanilla JS
44
-
45
- ### 번들러 사용
46
-
47
- ```ts
48
- import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg';
11
+ ```js
12
+ import { AerizEditor } from '@aeriz/wysiwyg';
49
13
  import '@aeriz/wysiwyg/style.css';
50
14
 
51
15
  const editor = new AerizEditor({
52
- element: document.querySelector('#editor')!,
16
+ element: document.querySelector('#editor'),
53
17
  initialHTML: '<p>안녕하세요</p>',
54
- placeholder: '내용을 입력하세요',
55
- theme: 'dark',
56
- onChange: (html) => localStorage.setItem('draft', html),
18
+ height: 480,
19
+ upload: { url: '/api/upload' }, // POST multipart "file" → { "url": "…" }
20
+ onChange: (html) => save(html),
57
21
  });
58
22
 
59
- const viewer = new AerizViewer({
60
- element: document.querySelector('#preview')!,
61
- });
62
- editor.onStateChange(() => viewer.setBlocks(editor.serialize()));
23
+ editor.getHTML(); // HTML with inline styles, safe to paste anywhere
24
+ editor.getBlocks(); // JSON snapshots for storage
25
+ editor.setReadOnly(true);
63
26
  ```
64
27
 
65
- ### CDN — 스크립트 태그
66
-
67
- 빌드 단계 없이 바로 사용. 라이브러리는 글로벌 `window.AerizWysiwyg` 와 단축
68
- 별칭 `window.AerizEditor` / `window.AerizViewer` 로 노출됩니다.
28
+ ## Why
69
29
 
70
- ```html
71
- <!-- ✓ 프로덕션 — 특정 버전 고정 (immutable, 가장 긴 캐시) -->
72
- <link
73
- rel="stylesheet"
74
- href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
75
- />
76
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
77
-
78
- <!-- 1.x 트랙 — 패치만 자동 수신 -->
79
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.5/dist/aeriz-wysiwyg.min.js"></script>
80
-
81
- <!-- 항상 최신 — 프로토타이핑용 (프로덕션 권장 X) -->
82
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@latest/dist/aeriz-wysiwyg.min.js"></script>
30
+ - **Tables like a word processor.** Row and column insert, delete, merge and split keep a merged-cell table rectangular. Tab walks cells in grid order.
31
+ - **Clean paste.** Pasting from Word, Hangul or Excel keeps bold, colors, borders, merges and lists, and drops `mso-*`, classes and comments. Word's fake lists become real `<ul>` and `<ol>`. `Ctrl+Shift+V` pastes plain text.
32
+ - **Korean document formatting.** Font sizes are in pt (10pt, 10.5pt, …). You get Korean system fonts (맑은 고딕, 굴림, 바탕, 나눔고딕 …), line height in %, letter spacing, special characters, find and replace (`Ctrl+H`), HTML source view, print and full screen. No web fonts are downloaded at runtime.
33
+ - **Safe and fast.** Every path that renders HTML (paste, `setHTML`, viewer, server render) sanitizes it. A keystroke in a 65-page document takes about 18 ms, and a 190-page document opens in about half a second.
34
+ - **Accessible.** The editor has an accessible name, and the toolbar is one tab stop with arrow-key navigation. `Alt+F10` jumps to the toolbar and `Esc` returns to the text.
35
+ - **Only what you use.** The code-block highlighter is a separate entry point, so it isn't bundled unless you import it.
83
36
 
84
- <!-- unpkg 미러 -->
85
- <script src="https://unpkg.com/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
37
+ ## Code highlighting (optional)
86
38
 
87
- <div id="editor"></div>
88
- <script>
89
- const editor = new AerizEditor({
90
- element: document.querySelector('#editor'),
91
- initialHTML: '<p>안녕하세요</p>',
92
- onChange: (html) => console.log(html),
93
- });
94
- </script>
95
- ```
96
-
97
- ### CDN — ES Module
39
+ ```js
40
+ import { highlighter } from '@aeriz/wysiwyg/highlight';
98
41
 
99
- ```html
100
- <link
101
- rel="stylesheet"
102
- href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
103
- />
104
- <script type="module">
105
- import {
106
- AerizEditor,
107
- AerizViewer,
108
- } from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js';
109
-
110
- new AerizEditor({ element: document.querySelector('#editor') });
111
- </script>
42
+ new AerizEditor({ element, highlighter }); // 15 languages, about 4 KB gzip
112
43
  ```
113
44
 
114
- ---
115
-
116
- ## CDN — 어댑터도 스크립트 태그로
45
+ Without `highlighter`, code blocks show plain text and the language picker is hidden. You can pass your own `{ languages, highlight(code, id) }`. Its output is filtered to `<span class>` only.
117
46
 
118
- `dist/adapters/*.js` 는 ESM 이라 `<script type="module">` 로 CDN 에서 바로
119
- 부르면 bare import 를 해석하지 못합니다. 그래서 **프레임워크 의존이 없는
120
- 어댑터는 IIFE 로도 배포**합니다. 코어를 번들에 넣지 않고 전역
121
- `AerizWysiwyg` 를 참조하므로, 코어를 먼저 부르기만 하면 됩니다 (코어가 두 벌
122
- 로드되지 않습니다).
47
+ ## Frameworks
123
48
 
124
- | 파일 | 노출 전역 | 역할 |
125
- | --- | --- | --- |
126
- | `dist/aeriz-wysiwyg-wc.min.js` | `AerizWysiwygWC` | `<aeriz-editor>` 자동 등록 |
127
- | `dist/aeriz-wysiwyg-jquery.min.js` | `AerizWysiwygJQuery` | `$.fn.aerizEditor` 자동 등록 |
128
- | `dist/aeriz-wysiwyg-adapter.min.js` | `AerizWysiwygAdapter` | `createEditorController` 등 |
49
+ Every adapter takes the same options as `new AerizEditor()`.
129
50
 
130
- ```html
131
- <link
132
- rel="stylesheet"
133
- href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
134
- />
135
- <!-- 1) 코어 먼저 -->
136
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
137
- <!-- 2) 필요한 어댑터 -->
138
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-wc.min.js"></script>
139
-
140
- <aeriz-editor theme="dark" placeholder="내용을 입력하세요"></aeriz-editor>
51
+ ```jsx
52
+ // React 16.8+
53
+ import { AerizEditor } from '@aeriz/wysiwyg/react';
54
+ <AerizEditor value={html} onChange={setHtml} height={480} upload={{ url: '/api/upload' }} />;
141
55
  ```
142
56
 
143
- jQuery 는 jQuery 자체를 먼저 로드해야 자동 등록됩니다.
144
-
145
- ```html
146
- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
147
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
148
- <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-jquery.min.js"></script>
149
- <script>
150
- $('#editor').aerizEditor({ value: '<p>안녕하세요</p>' });
151
- </script>
57
+ ```vue
58
+ <!-- Vue 3 (Vue 2.6/2.7: @aeriz/wysiwyg/vue2) -->
59
+ <AerizEditor v-model="html" :height="480" :read-only="locked" @error="onError" />
60
+ <!-- import { AerizEditor } from '@aeriz/wysiwyg/vue' -->
152
61
  ```
153
62
 
154
- ### React · Vue 를 CDN 으로 쓸 때 — import map
155
-
156
- React / Vue 어댑터는 IIFE 로 제공하지 않습니다. 소비자 앱의 프레임워크
157
- 인스턴스를 그대로 써야 하는데 전역 하나로 묶으면 인스턴스가 갈라져 훅이
158
- 깨지기 때문입니다. CDN 에서는 import map 이 정답입니다.
159
-
160
- ```html
161
- <script type="importmap">
162
- {
163
- "imports": {
164
- "vue": "https://esm.sh/vue@3",
165
- "@aeriz/wysiwyg": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js",
166
- "@aeriz/wysiwyg/vue": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/adapters/vue.js"
167
- }
168
- }
169
- </script>
170
- <script type="module">
171
- import { createApp, h, ref } from 'vue';
172
- import { AerizEditor } from '@aeriz/wysiwyg/vue';
173
-
174
- const html = ref('<p>안녕하세요</p>');
175
- createApp({
176
- render: () =>
177
- h(AerizEditor, {
178
- modelValue: html.value,
179
- 'onUpdate:modelValue': (v) => (html.value = v),
180
- }),
181
- }).mount('#app');
182
- </script>
63
+ ```svelte
64
+ <!-- Svelte 3, 4, 5 -->
65
+ <div use:aerizEditor={{ value: html, onChange: (v) => (html = v) }}></div>
66
+ <!-- import { aerizEditor } from '@aeriz/wysiwyg/svelte' -->
183
67
  ```
184
68
 
185
- > `@aeriz/wysiwyg` 항목이 반드시 있어야 합니다 — 어댑터가 그 이름으로 코어를
186
- > 가져오기 때문입니다. 빠뜨리면 브라우저가 bare specifier 해석에 실패합니다.
187
-
188
- ---
189
-
190
- ## Web Component
191
-
192
- 프레임워크 의존이 전혀 없는 최소 공통 분모입니다. Shadow DOM 을 쓰지 않으므로
193
- 전역 스타일시트가 그대로 적용됩니다.
194
-
195
69
  ```html
196
- <link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
197
- <script type="module">
198
- import '@aeriz/wysiwyg/wc'; // import 만으로 자동 등록
199
- </script>
200
-
201
- <form action="/posts" method="post">
202
- <!-- name 을 주면 폼 전송에 본문 HTML 이 함께 실립니다 (폼 연동 지원 브라우저) -->
203
- <aeriz-editor name="body" theme="dark" placeholder="내용을 입력하세요">
204
- </aeriz-editor>
205
- <button type="submit">저장</button>
206
- </form>
207
-
208
- <script type="module">
209
- const el = document.querySelector('aeriz-editor');
210
- el.addEventListener('change', (e) => console.log(e.detail.html));
211
- el.value = '<p>안녕하세요</p>';
212
- el.onImageUpload = async (file) => uploadAndGetUrl(file);
213
- </script>
70
+ <!-- Web Component -->
71
+ <aeriz-editor name="body" height="480" upload-url="/api/upload"></aeriz-editor>
72
+ <!-- import '@aeriz/wysiwyg/wc' -->
214
73
  ```
215
74
 
216
- | 속성 | 값 |
217
- | --- | --- |
218
- | `value` | 본문 HTML (프로퍼티로도 읽고 쓸 수 있음) |
219
- | `placeholder` | 빈 본문 안내 문구 |
220
- | `theme` | `light` / `dark` |
221
- | `locale` | `en` / `ko` / `ja` / `zh` |
222
- | `toolbar` | `false` · 항목 목록(`"bold italic"`) · JSON 객체 |
223
- | `image-upload-url`, `file-upload-url` | 업로드 API URL |
224
-
225
- 프로퍼티: `value` · `editor` · `getBlocks()` · `focus()` ·
226
- `onImageUpload` · `onFileUpload`.
227
- 이벤트: `input` · `change` (`detail.html`) · `aeriz-ready` (`detail.editor`).
228
-
229
- 태그 이름이 충돌하면 접두사를 바꿀 수 있습니다.
230
-
231
- ```ts
232
- import { defineAerizElements } from '@aeriz/wysiwyg/wc';
233
- defineAerizElements('my'); // <my-editor> / <my-viewer>
75
+ ```js
76
+ // jQuery
77
+ import '@aeriz/wysiwyg/jquery';
78
+ $('#editor').aerizEditor({ value: '<p>안녕하세요</p>' });
234
79
  ```
235
80
 
236
- ---
81
+ Angular 14+ ships as TypeScript source (`@aeriz/wysiwyg/angular`). See [angular/README.md](./angular/README.md).
237
82
 
238
- ## jQuery
83
+ ## CDN
239
84
 
240
85
  ```html
241
- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
242
- <link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
243
- <script type="module">
244
- import '@aeriz/wysiwyg/jquery'; // 전역 jQuery 가 있으면 자동 등록
245
-
246
- $('#editor').aerizEditor({
247
- value: '<p>안녕하세요</p>',
248
- theme: 'dark',
249
- onChange: (html) => console.log(html),
86
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@2.0.0/dist/aeriz-wysiwyg.min.css" />
87
+ <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@2.0.0/dist/aeriz-wysiwyg.min.js"></script>
88
+ <!-- optional: code highlighting, as the global AerizWysiwygHighlight -->
89
+ <script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@2.0.0/dist/aeriz-wysiwyg-highlight.min.js"></script>
90
+ <script>
91
+ new AerizWysiwyg.AerizEditor({
92
+ element: document.querySelector('#editor'),
93
+ highlighter: AerizWysiwygHighlight.highlighter,
250
94
  });
251
-
252
- const html = $('#editor').aerizEditor('getHTML');
253
- $('#editor').aerizEditor('setHTML', '<p>바꿈</p>');
254
- $('#editor').aerizEditor('destroy');
255
95
  </script>
256
96
  ```
257
97
 
258
- 번들러 환경에서 전역 jQuery 를 쓰지 않는다면 명시적으로 등록합니다.
259
-
260
- ```ts
261
- import $ from 'jquery';
262
- import { registerAerizJQuery } from '@aeriz/wysiwyg/jquery';
263
-
264
- registerAerizJQuery($);
265
- ```
266
-
267
- 명령: `getHTML` · `getBlocks` · `setHTML` · `update` · `focus` ·
268
- `instance` · `destroy`. `aerizViewer` 도 같은 방식으로 동작합니다.
269
-
270
- 같은 노드에 두 번 초기화하면 새 인스턴스를 만들지 않고 갱신으로 처리합니다.
271
-
272
- ---
273
-
274
- ## React
275
-
276
- ```tsx
277
- import { useState } from 'react';
278
- import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/react';
279
- import '@aeriz/wysiwyg/style.css';
280
-
281
- export function Compose() {
282
- const [html, setHtml] = useState('<p>안녕하세요</p>');
283
-
284
- return (
285
- <>
286
- <AerizEditor
287
- value={html}
288
- onChange={setHtml}
289
- theme="dark"
290
- placeholder="내용을 입력하세요"
291
- className="my-editor"
292
- imageUploadUrl="/api/upload"
293
- />
294
- <AerizViewer html={html} />
295
- </>
296
- );
297
- }
298
- ```
299
-
300
- `value` 를 그대로 되돌려줘도 캐럿이 튀지 않습니다 — 들어온 값이 에디터의 현재
301
- 출력과 같으면 본문을 교체하지 않기 때문입니다.
302
-
303
- `ref` 로 명령형 API 에 접근합니다.
304
-
305
- ```tsx
306
- const ref = useRef<AerizEditorHandle>(null);
307
- // ref.current.editor — 코어 인스턴스 (전체 API)
308
- // ref.current.getHTML() / setHTML(html) / focus()
309
- ```
310
-
311
- ---
312
-
313
- ## Vue 3
314
-
315
- ```vue
316
- <script setup>
317
- import { ref } from 'vue';
318
- import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/vue';
319
- import '@aeriz/wysiwyg/style.css';
320
-
321
- const html = ref('<p>안녕하세요</p>');
322
- const editor = ref(null);
323
- </script>
324
-
325
- <template>
326
- <AerizEditor
327
- ref="editor"
328
- v-model="html"
329
- theme="dark"
330
- placeholder="내용을 입력하세요"
331
- @state-change="onStateChange"
332
- @ready="onReady"
333
- />
334
- <AerizViewer :html="html" />
335
- </template>
336
- ```
337
-
338
- 전역 등록도 가능합니다.
339
-
340
- ```ts
341
- import { AerizWysiwygPlugin } from '@aeriz/wysiwyg/vue';
342
- app.use(AerizWysiwygPlugin);
343
- ```
344
-
345
- 이벤트: `update:modelValue` · `change` · `state-change` · `ready`.
346
- `ref` 로 노출되는 것: `editor` · `getHTML()` · `setHTML()` · `focus()`.
347
-
348
- ## Vue 2 (2.6 / 2.7)
98
+ Web Component and jQuery adapters also come as script tags: `aeriz-wysiwyg-wc.min.js` and `aeriz-wysiwyg-jquery.min.js`. Load the core first.
349
99
 
350
- Vue 2 관례에 따라 `v-model` 은 `value` prop + `input` 이벤트를 씁니다.
100
+ ## Server and edge (no DOM)
351
101
 
352
102
  ```js
353
- import { AerizEditor } from '@aeriz/wysiwyg/vue2';
354
- import '@aeriz/wysiwyg/style.css';
355
-
356
- export default {
357
- components: { AerizEditor },
358
- data: () => ({ html: '<p>안녕하세요</p>' }),
359
- template: `
360
- <AerizEditor ref="editor" v-model="html" theme="dark" />
361
- `,
362
- methods: {
363
- focusEditor() {
364
- this.$refs.editor.focus();
365
- },
366
- },
367
- };
368
- ```
369
-
370
- 이 어댑터는 `vue` 를 import 하지 않습니다 (Vue 2 컴포넌트는 순수 옵션 객체).
371
- 따라서 소비자 앱의 Vue 인스턴스와 이중화될 여지가 없습니다.
372
-
373
- ---
374
-
375
- ## Svelte (3 / 4 / 5)
376
-
377
- 컴파일된 `.svelte` 컴포넌트가 아니라 **액션**으로 제공합니다. 액션은 순수 함수
378
- 계약이라 Svelte 메이저 버전 세 개를 배포본 하나로 만족시킵니다.
379
-
380
- ```svelte
381
- <script>
382
- import { aerizEditor, aerizViewer } from '@aeriz/wysiwyg/svelte';
383
- import '@aeriz/wysiwyg/style.css';
384
-
385
- let html = '<p>안녕하세요</p>';
386
- </script>
387
-
388
- <div
389
- use:aerizEditor={{
390
- value: html,
391
- onChange: (next) => (html = next),
392
- theme: 'dark',
393
- placeholder: '내용을 입력하세요',
394
- }}
395
- />
396
-
397
- <div use:aerizViewer={{ html }} />
398
- ```
399
-
400
- ---
401
-
402
- ## Angular (14+)
103
+ import { renderBlocksToHTML, blocksToMarkdown, blocksToPlainText } from '@aeriz/wysiwyg/server';
403
104
 
404
- Angular 연동은 **TypeScript 소스로 배포**됩니다 — 소비자의 Angular 컴파일러가
405
- 직접 컴파일하므로 Angular 14 이상 어느 버전에서도 같은 파일이 동작합니다.
406
- 설정 방법(택 1)과 상세 문서는 [angular/README.md](./angular/README.md) 를
407
- 참고하세요.
408
-
409
- `tsconfig.app.json` 의 `include` 에 한 줄 추가하면 끝입니다.
410
-
411
- ```jsonc
412
- "include": ["src/**/*.ts", "node_modules/@aeriz/wysiwyg/angular/*.ts"]
413
- ```
414
-
415
- ```ts
416
- import { Component } from '@angular/core';
417
- import { FormsModule } from '@angular/forms';
418
- import { AerizEditorDirective } from '@aeriz/wysiwyg/angular';
419
-
420
- @Component({
421
- standalone: true,
422
- imports: [FormsModule, AerizEditorDirective],
423
- template: `
424
- <div aerizEditor [(ngModel)]="html" theme="dark"></div>
425
- <div aerizViewer [html]="html"></div>
426
- `,
427
- })
428
- export class ComposeComponent {
429
- html = '<p>안녕하세요</p>';
430
- }
431
- ```
432
-
433
- `[(value)]` · `[(ngModel)]` · `formControlName` 세 가지 바인딩을 모두
434
- 지원합니다. 디렉티브는 에디터를 `NgZone` 밖에서 생성하므로 캐럿을 움직일 때마다
435
- 변경 감지가 돌지 않습니다.
436
-
437
- ---
438
-
439
- ## 서버 / 엣지 런타임 (DOM 없음)
440
-
441
- 저장된 `BlockSnapshot[]` 을 HTML · 마크다운 · 평문으로 바꿉니다. **CSS 를
442
- import 하지 않고**, import 그래프 전체가 `document` / `window` 를 참조하지
443
- 않습니다 — Cloudflare Workers(workerd) · Node · Deno · Bun 에서 그대로
444
- 동작합니다.
445
-
446
- ```ts
447
- import {
448
- renderBlocksToHTML,
449
- blocksToMarkdown,
450
- blocksToPlainText,
451
- } from '@aeriz/wysiwyg/server';
452
-
453
- // 발행용 HTML — 같은 입력이면 항상 같은 출력 (결정적 · 멱등)
454
105
  const html = renderBlocksToHTML(post.blocks, { wrapper: 'content' });
455
-
456
- // 포맷 종속을 푸는 탈출구
457
- const markdown = blocksToMarkdown(post.blocks);
458
-
459
- // 전문 검색 색인용
460
- const text = blocksToPlainText(post.blocks, { blockSeparator: ' ' });
461
- ```
462
-
463
- ```ts
464
- // Cloudflare Worker
465
- export default {
466
- async fetch(req, env) {
467
- const post = await env.DB.get('post:1', 'json');
468
- return new Response(renderBlocksToHTML(post.blocks, { wrapper: 'content' }), {
469
- headers: { 'content-type': 'text/html; charset=utf-8' },
470
- });
471
- },
472
- };
473
106
  ```
474
107
 
475
- ---
108
+ It doesn't import CSS or touch `document`, so it runs in Node, Deno, Bun and Cloudflare Workers.
476
109
 
477
- ## 키보드 단축키
110
+ ## Upgrading from 1.x
478
111
 
479
- macOS 는 `Ctrl` 대신 `Cmd`. 전체 목록과 근거는
480
- [SPEC.md §7](./SPEC.md#7-키보드-단축키--입력-규칙) 에 있습니다.
112
+ 2.0 cleans up the public API. Most apps need only the renames below.
481
113
 
482
- | 동작 | 단축키 |
114
+ | 1.x | 2.0 |
483
115
  | --- | --- |
484
- | 되돌리기 / 다시 실행 | `Ctrl+Z` / `Ctrl+Shift+Z` · `Ctrl+Y` |
485
- | 굵게 · 기울임 · 밑줄 · 취소선 | `Ctrl+B` · `Ctrl+I` · `Ctrl+U` · `Ctrl+Shift+X` |
486
- | **본문**으로 | `Ctrl+Alt+0` |
487
- | 제목 1~6 | `Ctrl+Alt+1` ~ `Ctrl+Alt+6` |
488
- | 번호 목록 · 글머리 목록 · 체크리스트 | `Ctrl+Alt+7` · `Ctrl+Alt+8` · `Ctrl+Alt+T` |
489
- | 인용구 · 코드 블록 | `Ctrl+Alt+9` · `Ctrl+Alt+C` |
490
- | 블록 이동 / 삭제 | `Ctrl+Shift+↑` `Ctrl+Shift+↓` / `Ctrl+Shift+D` |
491
- | 표: 다음 / 이전 셀 | `Tab` / `Shift+Tab` (마지막 셀에서 `Tab` → 새 행) |
492
- | 목록: 들여쓰기 / 내어쓰기 | `Tab` / `Shift+Tab` |
493
-
494
- **마크다운처럼 입력해도 됩니다.** 블록 맨 앞에서 `# ` `- ` `1. ` `> ` `` ``` ``
495
- `---` `- [ ] ` 를 치면 그 자리에서 유형이 바뀝니다.
496
-
497
- > 새로 만들어지는 블록의 기본 유형은 언제나 **본문**입니다. 제목 끝에서
498
- > `Enter` 를 눌러도 제목이 복제되지 않고 본문이 이어지며, 빈 제목·인용구·
499
- > 코드 블록에서 `Backspace` 를 누르면 본문으로 돌아옵니다.
500
-
501
- ## 서버 sanitizer 와 함께 쓰기
502
-
503
- `getHTML()` 은 기본적으로 테마 색을 인라인 `style` 에 베이크합니다 —
504
- 스타일시트 없이 어디에 붙여도 같은 시각으로 보이지만, `style` 속성을 제거하는
505
- sanitizer 를 통과하면 서식이 통째로 사라집니다.
506
-
507
- 두 가지 대응이 있습니다.
508
-
509
- **1. 클래스 기반 출력으로 전환** — `style` 프로퍼티가 40여 개에서 10개로
510
- 줄고, 정렬은 클래스로 표현됩니다.
511
-
512
- ```ts
513
- new AerizEditor({ element, htmlStyles: 'class' }); // getHTML/onChange 기본값
514
- editor.getHTML({ styles: 'class' }); // 호출 단위 재정의
515
- ```
516
-
517
- 출력은 `aeriz-wysiwyg.min.css` 를 로드한 페이지의
518
- `.aeriz-wysiwyg > .aw-content` 안에 넣어야 기본 서식이 적용됩니다.
519
- `renderBlocksToHTML(blocks, { wrapper: 'content' })` 가 그 컨테이너를 만들어
520
- 줍니다.
521
-
522
- **2. 허용목록을 출력에 맞추기** — 등장할 수 있는 태그 · 속성 · style
523
- 프로퍼티의 확정 목록이
524
- [SPEC.md §9 출력 HTML 허용목록](./SPEC.md#9-출력-html-허용목록) 에 있습니다.
525
- 추측이 아니라 실측이며 `npm run allowlist` 로 재생성할 수 있습니다.
526
-
527
- > ⚠️ **HTML 가져오기 기능은 sanitizer 가 아닙니다.** 원본 스크립트가 실행되고
528
- > 이벤트 핸들러 속성이 제거되지 않습니다. 저장 전 서버 정화가 필수입니다 —
529
- > [SPEC.md §11](./SPEC.md#11-html-가져오기의-보안-경계) 참고.
530
-
531
- ## 스냅샷 스키마 버전
532
-
533
- `editor.serialize()` 는 각 블록에 `version: 1` 을 기록합니다. 저장된 데이터를
534
- 읽는 모든 경로가 `upgradeSnapshots()` 를 통과하므로, 버전이 올라가도 예전
535
- 데이터가 그대로 열립니다. 변경 정책은
536
- [SPEC.md §10](./SPEC.md#10-blocksnapshot-스키마-버전-정책) 에 있습니다.
537
-
538
- ## 직접 어댑터 만들기
539
-
540
- 목록에 없는 프레임워크(Solid, Qwik, Lit …) 는 프레임워크 중립 컨트롤러 위에
541
- 얇게 올리면 됩니다. 값 바인딩 · 캐럿 보존 · 재생성 판단이 이미 들어 있습니다.
542
-
543
- ```ts
544
- import { createEditorController } from '@aeriz/wysiwyg/adapter';
545
-
546
- const controller = createEditorController(hostElement, {
547
- value: '<p>안녕하세요</p>',
548
- onChange: (html) => setState(html),
549
- });
550
-
551
- controller.update({ value: nextHtml }); // 바뀐 것만 반영
552
- controller.destroy(); // 언마운트 시
553
- ```
554
-
555
- ## 빌드
116
+ | `editor.serialize()` | `editor.getBlocks()` (now returns `BlockSnapshot[]`) |
117
+ | `editor.getRawHTML()` | `editor.getHTML()` |
118
+ | `onImageUpload` · `onFileUpload` | `upload: { handler: (file, kind) => url }` |
119
+ | `imageUploadUrl` · `fileUploadUrl` | `upload: { url, headers, maxSize }` |
120
+ | `<aeriz-editor image-upload-url file-upload-url>` | `upload-url` attribute or the `upload` property |
121
+ | Code highlighting always on | `highlighter` option from `@aeriz/wysiwyg/highlight` |
122
+ | Font size in px | pt (`fontSizes: [10, 10.5, 12, …]`) |
123
+ | Google Fonts in the font list | System Korean fonts. Pass `fonts` for your own, with `cssUrl` for web fonts |
124
+ | `setLocale` · `t` · `getLocale` | the `locale` option |
125
+ | `renderBlockToHTML` · `upgradeSnapshot` · `snapshotVersionOf` | `renderBlocksToHTML` · `upgradeSnapshots` |
126
+ | Block internals (`BLOCK_TYPES`, registries) | no longer exported |
127
+
128
+ New options: `readOnly`, `height`, `upload`, `onError`, `fonts`, `fontSizes`, `ariaLabel`, `highlighter`. New methods: `setReadOnly()`, `setBlocks()`, `openFind()`, `toggleSource()`, `toggleFullscreen()`. Saved snapshots from 1.x open unchanged.
129
+
130
+ ## Development
556
131
 
557
132
  ```bash
558
- npm install
559
- npm run build # dist/ 에 IIFE / ESM / CJS / 어댑터 / CSS / .d.ts 생성
560
- npm test # 서버 렌더러 · getHTML · 어댑터 계약 테스트 (jsdom)
561
- npm run typecheck
562
- npm run allowlist # 출력 HTML 허용목록 재생성
563
- npm run version:sync # 문서·데모의 CDN 버전 표기를 package.json 에 맞춤
564
- npm run version:check # 뒤처졌으면 실패 (CI 용)
565
- npm run dev # 데모 페이지 (http://localhost:5173)
566
-
567
- npm run release:stage # npm 업로드용 release/ 조립 + exports 경로 검증
568
- npm run release:verify # release/ 에서 npm pack --dry-run
133
+ npm run build -w packages/wysiwyg # dist/ and dist-types/
134
+ npm test -w packages/wysiwyg # unit tests (jsdom)
135
+ npm run test:editor -w packages/wysiwyg # real-browser editing checks (Playwright + Chrome)
136
+ npm run dev -w packages/wysiwyg # demo at http://localhost:5173
569
137
  ```
570
138
 
571
- `dist/` 는 데모 서버의 publicDir 을 겸하고 개발용 비압축본·CDN 번들까지
572
- 담고 있습니다. 실제로 npm 에 올라가는 것은 `release:stage` 가 `"files"` 만
573
- 골라 조립한 `release/` 입니다. 배포 절차는 [RELEASE.md](./RELEASE.md) (붙여넣어
574
- 실행하는 런북), 계정·권한·회수 등 배경은 [PUBLISHING.md](./PUBLISHING.md) 를
575
- 참고하세요.
576
-
577
- 데모는 세 페이지입니다.
578
-
579
- - `/index.html` — 에디터 기능 데모
580
- - `/docs.html` — **연동 · 직렬화 레퍼런스**. 이 README 의 내용을 사이드바 · 검색 가능한
581
- API 표 · 라이브 플레이그라운드로 정리한 단일 페이지 문서입니다. 플레이그라운드에서
582
- 같은 문서가 `inline` / `class` HTML · 마크다운 · 평문 · 스냅샷 · 서버 렌더 여섯 가지로
583
- 어떻게 나오는지 실시간 비교할 수 있습니다.
584
- - `/frameworks.html` — 프레임워크 어댑터 데모. React · Vue 3 · Web Component ·
585
- Svelte 액션 · jQuery 를 한 페이지에 올리고, 로드 직후 자동 점검을 실행합니다.
586
-
587
- ### 문서 사이트 배포 — Cloudflare Workers
588
-
589
- `/docs.html` 을 그대로 정적 사이트로 올릴 수 있습니다. `docs:build` 가
590
- `examples/docs.html` 과 라이브러리 산출물을 `docs/` 에 모으고, 코드가 없는
591
- **정적 자산 전용 Worker** (`wrangler.toml`) 가 그 폴더를 서빙합니다. 별도
592
- Pages 프로젝트가 필요 없습니다.
593
-
594
- ```bash
595
- npx wrangler login # 처음 한 번만
596
-
597
- npm run docs:build # docs/ 조립
598
- npm run docs:preview # 로컬 Workers 런타임으로 확인 (wrangler dev)
599
- npm run docs:deploy # 배포
600
- ```
601
-
602
- 페이지 HTML 은 데모와 배포본이 같은 파일입니다 — `examples/docs.html` 이
603
- 라이브러리를 루트 절대 경로로 부르고, 배포 시에도 산출물을 사이트 루트에 같이
604
- 두기 때문에 경로 분기가 없습니다. 캐시·보안 헤더는 `docs/_headers` 로 함께
605
- 생성됩니다. `docs/` 는 파생물이라 git 에 추적하지 않습니다.
606
-
607
- ### 오프라인 단일 파일
608
-
609
- 문서 페이지를 외부에 공유하거나 오프라인으로 열어야 하면 라이브러리를 인라인한
610
- 단일 파일로 만들 수 있습니다 (외부 요청 0건).
611
-
612
- ```bash
613
- node scripts/build-docs-artifact.mjs docs-standalone.html
614
- ```
615
-
616
- 설치된 Chrome 으로 그 페이지를 실제 검증할 수 있습니다 (playwright-core 는
617
- 브라우저를 내려받지 않고 시스템 Chrome 을 그대로 씁니다).
618
-
619
- ```bash
620
- npm run dev # 다른 터미널에서
621
- npm run test:browser # 헤드리스 검증 + 스크린샷
622
- node scripts/browser-check.mjs --headed # 창을 띄워서 보기
623
- ```
624
-
625
- 5173 이 이미 쓰이는 중이면 데모를 다른 포트로 띄우고 주소를 넘기면 됩니다.
626
-
627
- ```bash
628
- npx vite --config vite.demo.config.ts --port 5347
629
- node scripts/browser-check.mjs http://localhost:5347/frameworks.html
630
- ```
139
+ The spec is [SPEC.md](./SPEC.md). To publish, follow [PUBLISHING.md](./PUBLISHING.md).
631
140
 
632
- ## 라이선스
141
+ ## License
633
142
 
634
143
  Apache-2.0