hwpkit-dev 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ .npmignore +4 -1
- package/README.md +39 -2
- package/dist/index.d.mts +74 -16
- package/dist/index.d.ts +70 -16
- package/dist/index.js +4985 -698
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4981 -698
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
- package/playground/index.html +346 -0
- package/playground/main.ts +302 -0
- package/playground/vite.config.ts +16 -0
- package/src/contract/decoder.ts +1 -0
- package/src/contract/encoder.ts +6 -1
- package/src/core/BaseDecoder.ts +118 -0
- package/src/core/BaseEncoder.ts +146 -0
- package/src/decoders/docx/DocxDecoder.ts +867 -150
- package/src/decoders/html/HtmlDecoder.ts +366 -0
- package/src/decoders/hwp/HwpScanner.ts +477 -88
- package/src/decoders/hwpx/HwpxDecoder.ts +789 -293
- package/src/decoders/md/MdDecoder.ts +4 -4
- package/src/encoders/docx/DocxEncoder.ts +600 -295
- package/src/encoders/html/HtmlEncoder.ts +203 -0
- package/src/encoders/hwp/HwpEncoder.ts +1647 -398
- package/src/encoders/hwpx/HwpxEncoder.ts +1512 -444
- package/src/encoders/hwpx/constants.ts +148 -0
- package/src/encoders/hwpx/utils.ts +198 -0
- package/src/encoders/md/MdEncoder.ts +117 -30
- package/src/index.ts +1 -0
- package/src/model/builders.ts +8 -6
- package/src/model/doc-props.ts +19 -5
- package/src/model/doc-tree.ts +13 -5
- package/src/pipeline/Pipeline.ts +21 -4
- package/src/pipeline/registry.ts +13 -2
- package/src/safety/StyleBridge.ts +52 -7
- package/src/toolkit/ArchiveKit.ts +56 -0
- package/src/toolkit/StyleMapper.ts +221 -0
- package/src/toolkit/UnitConverter.ts +138 -0
- package/src/toolkit/XmlKit.ts +0 -5
- package/test-styling.ts +210 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hwpkit-dev",
|
|
3
3
|
"description": "HWP/HWPX/DOCX/MD 양방향 문서 변환 라이브러리",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "INMD1",
|
|
7
7
|
"email": "lyw514549@gmail.com",
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"playground": "vite --config playground/vite.config.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"fs": "^0.0.1-security",
|
|
35
|
+
"jszip": "^3.10.1",
|
|
34
36
|
"pako": "^2.1.0",
|
|
37
|
+
"path": "^0.12.7",
|
|
35
38
|
"saxes": "^6.0.0"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>hwpkit playground</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
12
|
+
background: #0f1117;
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
header {
|
|
20
|
+
padding: 12px 20px;
|
|
21
|
+
background: #1a1d27;
|
|
22
|
+
border-bottom: 1px solid #2d3248;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 12px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
header h1 {
|
|
29
|
+
font-size: 16px;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
color: #a78bfa;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
header span {
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
color: #64748b;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.toolbar {
|
|
40
|
+
padding: 10px 20px;
|
|
41
|
+
background: #141722;
|
|
42
|
+
border-bottom: 1px solid #2d3248;
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 12px;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.toolbar label {
|
|
50
|
+
font-size: 13px;
|
|
51
|
+
color: #94a3b8;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
select {
|
|
55
|
+
background: #1e2236;
|
|
56
|
+
border: 1px solid #374151;
|
|
57
|
+
color: #e2e8f0;
|
|
58
|
+
padding: 5px 10px;
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
select:focus { outline: none; border-color: #a78bfa; }
|
|
65
|
+
|
|
66
|
+
button {
|
|
67
|
+
padding: 6px 16px;
|
|
68
|
+
border-radius: 6px;
|
|
69
|
+
border: none;
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.btn-primary {
|
|
76
|
+
background: #7c3aed;
|
|
77
|
+
color: #fff;
|
|
78
|
+
}
|
|
79
|
+
.btn-primary:hover { background: #6d28d9; }
|
|
80
|
+
|
|
81
|
+
.btn-secondary {
|
|
82
|
+
background: #1e2236;
|
|
83
|
+
color: #94a3b8;
|
|
84
|
+
border: 1px solid #374151;
|
|
85
|
+
}
|
|
86
|
+
.btn-secondary:hover { background: #252a40; }
|
|
87
|
+
|
|
88
|
+
.btn-upload {
|
|
89
|
+
background: #1e4036;
|
|
90
|
+
color: #34d399;
|
|
91
|
+
border: 1px solid #065f46;
|
|
92
|
+
}
|
|
93
|
+
.btn-upload:hover { background: #1a3a30; }
|
|
94
|
+
|
|
95
|
+
#file-input { display: none; }
|
|
96
|
+
|
|
97
|
+
.panels {
|
|
98
|
+
flex: 1;
|
|
99
|
+
display: grid;
|
|
100
|
+
grid-template-columns: 1fr 1fr;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.panel {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.panel:first-child { border-right: 1px solid #2d3248; }
|
|
111
|
+
|
|
112
|
+
.panel-header {
|
|
113
|
+
padding: 8px 16px;
|
|
114
|
+
background: #141722;
|
|
115
|
+
border-bottom: 1px solid #2d3248;
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
color: #64748b;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: space-between;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.panel-header strong { color: #94a3b8; font-weight: 600; }
|
|
124
|
+
|
|
125
|
+
.tab-bar {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.tab {
|
|
131
|
+
padding: 4px 10px;
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
background: transparent;
|
|
134
|
+
color: #64748b;
|
|
135
|
+
border: 1px solid transparent;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tab.active {
|
|
141
|
+
background: #1e2236;
|
|
142
|
+
color: #a78bfa;
|
|
143
|
+
border-color: #374151;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
textarea {
|
|
147
|
+
flex: 1;
|
|
148
|
+
background: #0f1117;
|
|
149
|
+
color: #e2e8f0;
|
|
150
|
+
border: none;
|
|
151
|
+
padding: 16px;
|
|
152
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
line-height: 1.6;
|
|
155
|
+
resize: none;
|
|
156
|
+
outline: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
textarea::placeholder { color: #374151; }
|
|
160
|
+
|
|
161
|
+
.output-area {
|
|
162
|
+
flex: 1;
|
|
163
|
+
overflow: auto;
|
|
164
|
+
padding: 16px;
|
|
165
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
line-height: 1.6;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.output-text {
|
|
171
|
+
white-space: pre-wrap;
|
|
172
|
+
word-break: break-all;
|
|
173
|
+
color: #e2e8f0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.output-tree {
|
|
177
|
+
display: none;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.output-html {
|
|
181
|
+
display: none;
|
|
182
|
+
flex: 1;
|
|
183
|
+
border: none;
|
|
184
|
+
background: #fff;
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: 100%;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.output-tree.active, .output-text.active {
|
|
190
|
+
display: block;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.output-html.active {
|
|
194
|
+
display: block;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.tree-node {
|
|
198
|
+
padding: 2px 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.tree-tag {
|
|
202
|
+
color: #60a5fa;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.tree-prop {
|
|
207
|
+
color: #fbbf24;
|
|
208
|
+
font-size: 11px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.tree-content {
|
|
212
|
+
color: #86efac;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tree-children {
|
|
216
|
+
margin-left: 20px;
|
|
217
|
+
border-left: 1px solid #1e2236;
|
|
218
|
+
padding-left: 12px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.warn-bar {
|
|
222
|
+
padding: 8px 16px;
|
|
223
|
+
background: #2d1a00;
|
|
224
|
+
border-top: 1px solid #78350f;
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
color: #fbbf24;
|
|
227
|
+
display: none;
|
|
228
|
+
max-height: 80px;
|
|
229
|
+
overflow-y: auto;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.warn-bar.visible { display: block; }
|
|
233
|
+
|
|
234
|
+
.status-bar {
|
|
235
|
+
padding: 4px 16px;
|
|
236
|
+
background: #0a0c14;
|
|
237
|
+
border-top: 1px solid #1e2236;
|
|
238
|
+
font-size: 11px;
|
|
239
|
+
color: #374151;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 16px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.status-ok { color: #34d399; }
|
|
246
|
+
.status-err { color: #f87171; }
|
|
247
|
+
|
|
248
|
+
@media (max-width: 700px) {
|
|
249
|
+
.panels { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }
|
|
250
|
+
.panel:first-child { border-right: none; border-bottom: 1px solid #2d3248; }
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
253
|
+
</head>
|
|
254
|
+
<body>
|
|
255
|
+
<header>
|
|
256
|
+
<h1>hwpkit</h1>
|
|
257
|
+
<span>문서 변환 라이브러리 playground</span>
|
|
258
|
+
</header>
|
|
259
|
+
|
|
260
|
+
<div class="toolbar">
|
|
261
|
+
<label>입력 포맷</label>
|
|
262
|
+
<select id="src-fmt">
|
|
263
|
+
<option value="md">Markdown (md)</option>
|
|
264
|
+
<option value="hwpx">HWPX</option>
|
|
265
|
+
<option value="docx">DOCX</option>
|
|
266
|
+
<option value="hwp">HWP (읽기 전용)</option>
|
|
267
|
+
</select>
|
|
268
|
+
|
|
269
|
+
<label>출력 포맷</label>
|
|
270
|
+
<select id="dst-fmt">
|
|
271
|
+
<option value="md">Markdown (md)</option>
|
|
272
|
+
<option value="html">HTML</option>
|
|
273
|
+
<option value="hwpx">HWPX</option>
|
|
274
|
+
<option value="docx">DOCX</option>
|
|
275
|
+
<option value="hwp">HWP (hwp)</option>
|
|
276
|
+
</select>
|
|
277
|
+
|
|
278
|
+
<button class="btn-primary" id="run-btn">변환 실행</button>
|
|
279
|
+
<button class="btn-secondary" id="inspect-btn">DocRoot 검사</button>
|
|
280
|
+
|
|
281
|
+
<label for="file-input">
|
|
282
|
+
<button class="btn-upload" onclick="document.getElementById('file-input').click()">파일 업로드</button>
|
|
283
|
+
</label>
|
|
284
|
+
<input type="file" id="file-input" accept=".md,.hwpx,.docx,.hwp,.txt" />
|
|
285
|
+
|
|
286
|
+
<span style="color:#374151;font-size:12px;">데모:</span>
|
|
287
|
+
<button class="btn-secondary" id="demo-hwpx-btn">demo.hwpx</button>
|
|
288
|
+
<button class="btn-secondary" id="demo-hwp-btn">demo.hwp</button>
|
|
289
|
+
|
|
290
|
+
<button class="btn-secondary" id="clear-btn">초기화</button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div class="panels">
|
|
294
|
+
<div class="panel">
|
|
295
|
+
<div class="panel-header">
|
|
296
|
+
<strong>입력</strong>
|
|
297
|
+
<span id="input-info">md · 0 bytes</span>
|
|
298
|
+
</div>
|
|
299
|
+
<textarea id="input" placeholder="Markdown 텍스트를 입력하거나 파일을 업로드하세요...
|
|
300
|
+
|
|
301
|
+
예시:
|
|
302
|
+
# 안녕하세요
|
|
303
|
+
|
|
304
|
+
**굵은 텍스트**와 *이탤릭*을 지원합니다.
|
|
305
|
+
|
|
306
|
+
## 목록
|
|
307
|
+
- 항목 1
|
|
308
|
+
- 항목 2
|
|
309
|
+
- 중첩 항목
|
|
310
|
+
|
|
311
|
+
## 표
|
|
312
|
+
|
|
313
|
+
| 이름 | 나이 |
|
|
314
|
+
|------|------|
|
|
315
|
+
| 홍길동 | 30 |
|
|
316
|
+
| 김철수 | 25 |
|
|
317
|
+
"></textarea>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div class="panel">
|
|
321
|
+
<div class="panel-header">
|
|
322
|
+
<strong>출력</strong>
|
|
323
|
+
<div class="tab-bar">
|
|
324
|
+
<button class="tab active" id="tab-text" onclick="switchTab('text')">텍스트</button>
|
|
325
|
+
<button class="tab" id="tab-html" onclick="switchTab('html')">HTML 미리보기</button>
|
|
326
|
+
<button class="tab" id="tab-tree" onclick="switchTab('tree')">트리</button>
|
|
327
|
+
</div>
|
|
328
|
+
<span id="output-info" style="font-size:11px;color:#64748b;"></span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="output-area">
|
|
331
|
+
<div class="output-text active" id="output-text"></div>
|
|
332
|
+
<iframe class="output-html" id="output-html"></iframe>
|
|
333
|
+
<div class="output-tree" id="output-tree"></div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="warn-bar" id="warn-bar"></div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div class="status-bar">
|
|
340
|
+
<span id="status">준비</span>
|
|
341
|
+
<span id="timing"></span>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<script type="module" src="./main.ts"></script>
|
|
345
|
+
</body>
|
|
346
|
+
</html>
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { Pipeline } from 'hwpkit';
|
|
2
|
+
import type { AnyNode, DocRoot } from 'hwpkit';
|
|
3
|
+
|
|
4
|
+
// ─── DOM refs ───────────────────────────────────────────────
|
|
5
|
+
const inputEl = document.getElementById('input') as HTMLTextAreaElement;
|
|
6
|
+
const srcFmtEl = document.getElementById('src-fmt') as HTMLSelectElement;
|
|
7
|
+
const dstFmtEl = document.getElementById('dst-fmt') as HTMLSelectElement;
|
|
8
|
+
const runBtn = document.getElementById('run-btn')!;
|
|
9
|
+
const inspectBtn = document.getElementById('inspect-btn')!;
|
|
10
|
+
const clearBtn = document.getElementById('clear-btn')!;
|
|
11
|
+
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
|
12
|
+
const outputText = document.getElementById('output-text')!;
|
|
13
|
+
const outputHtml = document.getElementById('output-html')!;
|
|
14
|
+
const outputTree = document.getElementById('output-tree')!;
|
|
15
|
+
const warnBar = document.getElementById('warn-bar')!;
|
|
16
|
+
const statusEl = document.getElementById('status')!;
|
|
17
|
+
const timingEl = document.getElementById('timing')!;
|
|
18
|
+
const inputInfo = document.getElementById('input-info')!;
|
|
19
|
+
const outputInfo = document.getElementById('output-info')!;
|
|
20
|
+
|
|
21
|
+
let rawBytes: Uint8Array | null = null;
|
|
22
|
+
let currentTab: 'text' | 'html' | 'tree' = 'text';
|
|
23
|
+
|
|
24
|
+
// ─── Demo 파일 로드 ──────────────────────────────────────────
|
|
25
|
+
async function loadDemoFile(path: string, fmt: string, label: string) {
|
|
26
|
+
setStatus(`${label} 로드 중...`, false);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(path);
|
|
29
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
30
|
+
rawBytes = new Uint8Array(await res.arrayBuffer());
|
|
31
|
+
srcFmtEl.value = fmt;
|
|
32
|
+
inputEl.value = `[데모 파일 로드됨: ${label} (${fmtBytes(rawBytes.length)})]`;
|
|
33
|
+
inputEl.style.color = '#a78bfa';
|
|
34
|
+
updateInputInfo();
|
|
35
|
+
setStatus(`${label} 로드 완료`, false);
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
setStatus(`데모 파일 로드 실패: ${e.message}`, true);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
document.getElementById('demo-hwpx-btn')!.addEventListener('click', () =>
|
|
42
|
+
loadDemoFile('/demo.hwpx', 'hwpx', 'demo.hwpx'));
|
|
43
|
+
document.getElementById('demo-hwp-btn')!.addEventListener('click', () =>
|
|
44
|
+
loadDemoFile('/demo.hwp', 'hwp', 'demo.hwp'));
|
|
45
|
+
|
|
46
|
+
// ─── 파일 업로드 ─────────────────────────────────────────────
|
|
47
|
+
fileInput.addEventListener('change', async () => {
|
|
48
|
+
const file = fileInput.files?.[0];
|
|
49
|
+
if (!file) return;
|
|
50
|
+
|
|
51
|
+
rawBytes = new Uint8Array(await file.arrayBuffer());
|
|
52
|
+
const ext = file.name.split('.').pop()?.toLowerCase() ?? 'md';
|
|
53
|
+
const fmt = ext === 'hwpx' ? 'hwpx' : ext === 'docx' ? 'docx' : ext === 'hwp' ? 'hwp' : 'md';
|
|
54
|
+
srcFmtEl.value = fmt;
|
|
55
|
+
|
|
56
|
+
inputEl.value = `[파일 로드됨: ${file.name} (${fmtBytes(rawBytes.length)})]`;
|
|
57
|
+
inputEl.style.color = '#60a5fa';
|
|
58
|
+
updateInputInfo();
|
|
59
|
+
setStatus(`파일 로드: ${file.name}`, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
inputEl.addEventListener('input', () => {
|
|
63
|
+
rawBytes = null;
|
|
64
|
+
inputEl.style.color = '';
|
|
65
|
+
updateInputInfo();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ─── 변환 ────────────────────────────────────────────────────
|
|
69
|
+
runBtn.addEventListener('click', async () => { await runConvert(); });
|
|
70
|
+
inspectBtn.addEventListener('click', async () => { await runInspect(); });
|
|
71
|
+
|
|
72
|
+
clearBtn.addEventListener('click', () => {
|
|
73
|
+
inputEl.value = '';
|
|
74
|
+
inputEl.style.color = '';
|
|
75
|
+
rawBytes = null;
|
|
76
|
+
clearOutput();
|
|
77
|
+
statusEl.textContent = '초기화됨';
|
|
78
|
+
statusEl.className = '';
|
|
79
|
+
timingEl.textContent = '';
|
|
80
|
+
updateInputInfo();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function clearOutput() {
|
|
84
|
+
outputText.textContent = '';
|
|
85
|
+
(outputHtml as HTMLIFrameElement).srcdoc = '';
|
|
86
|
+
outputTree.innerHTML = '';
|
|
87
|
+
outputInfo.textContent = '';
|
|
88
|
+
warnBar.classList.remove('visible');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getInput(): Promise<{ data: Uint8Array | string; fmt: string }> {
|
|
92
|
+
const fmt = srcFmtEl.value;
|
|
93
|
+
if (rawBytes) return { data: rawBytes, fmt };
|
|
94
|
+
return { data: inputEl.value, fmt: 'md' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runConvert() {
|
|
98
|
+
const t0 = performance.now();
|
|
99
|
+
setStatus('변환 중...', false);
|
|
100
|
+
clearOutput();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const { data, fmt } = await getInput();
|
|
104
|
+
const srcFmt = rawBytes ? fmt : 'md';
|
|
105
|
+
const dstFmt = dstFmtEl.value;
|
|
106
|
+
|
|
107
|
+
const pipeline = await Pipeline.openAsync(
|
|
108
|
+
typeof data === 'string' ? data : data,
|
|
109
|
+
srcFmt,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = await pipeline.to(dstFmt);
|
|
113
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
114
|
+
|
|
115
|
+
showWarns(result.warns ?? []);
|
|
116
|
+
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
showText(`오류: ${result.error}`);
|
|
119
|
+
setStatus(`실패: ${result.error}`, true);
|
|
120
|
+
timingEl.textContent = `${ms}ms`;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
outputInfo.textContent = `${fmtBytes(result.data.length)}`;
|
|
125
|
+
|
|
126
|
+
if (dstFmt === 'html') {
|
|
127
|
+
const html = new TextDecoder().decode(result.data);
|
|
128
|
+
showHtml(html);
|
|
129
|
+
} else if (dstFmt === 'md') {
|
|
130
|
+
showText(new TextDecoder().decode(result.data));
|
|
131
|
+
} else {
|
|
132
|
+
// Binary format: offer download
|
|
133
|
+
const blob = new Blob([result.data]);
|
|
134
|
+
const url = URL.createObjectURL(blob);
|
|
135
|
+
const a = document.createElement('a');
|
|
136
|
+
a.href = url;
|
|
137
|
+
a.download = `output.${dstFmt}`;
|
|
138
|
+
a.click();
|
|
139
|
+
URL.revokeObjectURL(url);
|
|
140
|
+
showText(`[${dstFmt.toUpperCase()} 파일 다운로드 완료]\n크기: ${fmtBytes(result.data.length)}\n\n다시 다운로드하려면 '변환 실행'을 눌러주세요.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setStatus(`변환 완료 (${srcFmt} → ${dstFmt})`, false);
|
|
144
|
+
timingEl.textContent = `${ms}ms`;
|
|
145
|
+
} catch (e: any) {
|
|
146
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
147
|
+
showText(`예외: ${e?.message ?? String(e)}\n\n${e?.stack ?? ''}`);
|
|
148
|
+
setStatus('예외 발생', true);
|
|
149
|
+
timingEl.textContent = `${ms}ms`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runInspect() {
|
|
154
|
+
const t0 = performance.now();
|
|
155
|
+
setStatus('검사 중...', false);
|
|
156
|
+
clearOutput();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const { data, fmt } = await getInput();
|
|
160
|
+
const srcFmt = rawBytes ? fmt : 'md';
|
|
161
|
+
|
|
162
|
+
const pipeline = await Pipeline.openAsync(
|
|
163
|
+
typeof data === 'string' ? data : data,
|
|
164
|
+
srcFmt,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const result = await pipeline.inspect();
|
|
168
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
169
|
+
|
|
170
|
+
showWarns(result.warns ?? []);
|
|
171
|
+
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
showText(`오류: ${result.error}`);
|
|
174
|
+
setStatus(`실패: ${result.error}`, true);
|
|
175
|
+
timingEl.textContent = `${ms}ms`;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
renderTree(result.data);
|
|
180
|
+
switchTab('tree');
|
|
181
|
+
setStatus('DocRoot 검사 완료', false);
|
|
182
|
+
timingEl.textContent = `${ms}ms`;
|
|
183
|
+
} catch (e: any) {
|
|
184
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
185
|
+
showText(`예외: ${e?.message ?? String(e)}`);
|
|
186
|
+
setStatus('예외 발생', true);
|
|
187
|
+
timingEl.textContent = `${ms}ms`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── 트리 렌더링 ──────────────────────────────────────────────
|
|
192
|
+
function renderTree(root: DocRoot) {
|
|
193
|
+
outputTree.innerHTML = '';
|
|
194
|
+
outputTree.appendChild(buildTreeEl(root as unknown as AnyNode));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildTreeEl(node: AnyNode): HTMLElement {
|
|
198
|
+
const wrap = document.createElement('div');
|
|
199
|
+
wrap.className = 'tree-node';
|
|
200
|
+
|
|
201
|
+
const tag = document.createElement('span');
|
|
202
|
+
tag.className = 'tree-tag';
|
|
203
|
+
tag.textContent = `<${node.tag}`;
|
|
204
|
+
wrap.appendChild(tag);
|
|
205
|
+
|
|
206
|
+
if ('props' in node && node.props && Object.keys(node.props).length > 0) {
|
|
207
|
+
const filtered = Object.entries(node.props).filter(([, v]) => v !== undefined && v !== null);
|
|
208
|
+
if (filtered.length > 0) {
|
|
209
|
+
const prop = document.createElement('span');
|
|
210
|
+
prop.className = 'tree-prop';
|
|
211
|
+
prop.textContent = ' ' + filtered.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ');
|
|
212
|
+
wrap.appendChild(prop);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const closeTag = document.createElement('span');
|
|
217
|
+
closeTag.className = 'tree-tag';
|
|
218
|
+
|
|
219
|
+
if (node.tag === 'txt') {
|
|
220
|
+
const ct = document.createElement('span');
|
|
221
|
+
ct.className = 'tree-content';
|
|
222
|
+
ct.textContent = ` "${(node as any).text ?? (node as any).content ?? ''}"`;
|
|
223
|
+
wrap.appendChild(ct);
|
|
224
|
+
closeTag.textContent = ' />';
|
|
225
|
+
wrap.appendChild(closeTag);
|
|
226
|
+
return wrap;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ('kids' in node && Array.isArray(node.kids) && node.kids.length > 0) {
|
|
230
|
+
closeTag.textContent = '>';
|
|
231
|
+
wrap.appendChild(closeTag);
|
|
232
|
+
|
|
233
|
+
const children = document.createElement('div');
|
|
234
|
+
children.className = 'tree-children';
|
|
235
|
+
for (const child of node.kids as AnyNode[]) {
|
|
236
|
+
children.appendChild(buildTreeEl(child));
|
|
237
|
+
}
|
|
238
|
+
wrap.appendChild(children);
|
|
239
|
+
|
|
240
|
+
const endTag = document.createElement('div');
|
|
241
|
+
endTag.className = 'tree-tag tree-node';
|
|
242
|
+
endTag.textContent = `</${node.tag}>`;
|
|
243
|
+
wrap.appendChild(endTag);
|
|
244
|
+
} else {
|
|
245
|
+
closeTag.textContent = ' />';
|
|
246
|
+
wrap.appendChild(closeTag);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return wrap;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── UI 헬퍼 ─────────────────────────────────────────────────
|
|
253
|
+
function showText(text: string) {
|
|
254
|
+
outputText.textContent = text;
|
|
255
|
+
if (currentTab !== 'text') switchTab('text');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function showHtml(html: string) {
|
|
259
|
+
const iframe = outputHtml as HTMLIFrameElement;
|
|
260
|
+
iframe.srcdoc = html;
|
|
261
|
+
switchTab('html');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function showWarns(warns: string[]) {
|
|
265
|
+
if (warns.length === 0) {
|
|
266
|
+
warnBar.classList.remove('visible');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
warnBar.textContent = warns.map(w => `⚠ ${w}`).join('\n');
|
|
270
|
+
warnBar.classList.add('visible');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function setStatus(msg: string, isErr: boolean) {
|
|
274
|
+
statusEl.textContent = msg;
|
|
275
|
+
statusEl.className = isErr ? 'status-err' : 'status-ok';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updateInputInfo() {
|
|
279
|
+
const fmt = srcFmtEl.value;
|
|
280
|
+
const bytes = rawBytes ? rawBytes.length : new TextEncoder().encode(inputEl.value).length;
|
|
281
|
+
inputInfo.textContent = `${fmt} · ${fmtBytes(bytes)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function fmtBytes(n: number): string {
|
|
285
|
+
if (n < 1024) return `${n} B`;
|
|
286
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
287
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── 탭 전환 ─────────────────────────────────────────────────
|
|
291
|
+
(window as any).switchTab = function (tab: 'text' | 'html' | 'tree') {
|
|
292
|
+
currentTab = tab;
|
|
293
|
+
outputText.classList.toggle('active', tab === 'text');
|
|
294
|
+
outputHtml.classList.toggle('active', tab === 'html');
|
|
295
|
+
outputTree.classList.toggle('active', tab === 'tree');
|
|
296
|
+
document.getElementById('tab-text')!.classList.toggle('active', tab === 'text');
|
|
297
|
+
document.getElementById('tab-html')!.classList.toggle('active', tab === 'html');
|
|
298
|
+
document.getElementById('tab-tree')!.classList.toggle('active', tab === 'tree');
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
srcFmtEl.addEventListener('change', updateInputInfo);
|
|
302
|
+
updateInputInfo();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
root: resolve(__dirname),
|
|
6
|
+
publicDir: resolve(__dirname, 'public'),
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'hwpkit': resolve(__dirname, '../src/index.ts'),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
define: {
|
|
13
|
+
'process.env': {},
|
|
14
|
+
global: 'globalThis',
|
|
15
|
+
},
|
|
16
|
+
});
|
package/src/contract/decoder.ts
CHANGED
package/src/contract/encoder.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { DocRoot } from '../model/doc-tree';
|
|
2
2
|
import type { Outcome } from './result';
|
|
3
3
|
|
|
4
|
+
export interface EncoderOptions {
|
|
5
|
+
[key: string]: any; // 포맷별 옵션 확장 가능
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export interface Encoder {
|
|
5
9
|
readonly format: string;
|
|
6
|
-
|
|
10
|
+
readonly aliases?: string[];
|
|
11
|
+
encode(doc: DocRoot, options?: EncoderOptions): Promise<Outcome<Uint8Array>>;
|
|
7
12
|
}
|