kordoc 3.8.0 → 3.8.2
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 +11 -1
- package/dist/{-ATVQYFSW.js → -FEHSMPVO.js} +3 -3
- package/dist/{chunk-QV25HMU7.js → chunk-553VTUVP.js} +2 -2
- package/dist/{chunk-SLKF72QF.js → chunk-DP37KF2X.js} +1985 -1954
- package/dist/chunk-DP37KF2X.js.map +1 -0
- package/dist/{chunk-3R3YK7EM.js → chunk-JHZUFBUV.js} +2 -2
- package/dist/{chunk-ITJIALN5.cjs → chunk-YBPNKFJW.cjs} +2 -2
- package/dist/{chunk-ITJIALN5.cjs.map → chunk-YBPNKFJW.cjs.map} +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.cjs +2059 -2028
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -8
- package/dist/index.d.ts +17 -8
- package/dist/index.js +1974 -1943
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/{parser-7G5F7PT2.js → parser-KNQDRLZQ.js} +1930 -1863
- package/dist/parser-KNQDRLZQ.js.map +1 -0
- package/dist/{parser-DCK42RMA.js → parser-NR2TYGO3.js} +1930 -1863
- package/dist/parser-NR2TYGO3.js.map +1 -0
- package/dist/{parser-GUSJH44K.cjs → parser-NS4ZPD7B.cjs} +1933 -1864
- package/dist/parser-NS4ZPD7B.cjs.map +1 -0
- package/dist/{watch-GVZESOCE.js → watch-XCWADLPU.js} +3 -3
- package/package.json +2 -2
- package/dist/chunk-SLKF72QF.js.map +0 -1
- package/dist/parser-7G5F7PT2.js.map +0 -1
- package/dist/parser-DCK42RMA.js.map +0 -1
- package/dist/parser-GUSJH44K.cjs.map +0 -1
- /package/dist/{-ATVQYFSW.js.map → -FEHSMPVO.js.map} +0 -0
- /package/dist/{chunk-QV25HMU7.js.map → chunk-553VTUVP.js.map} +0 -0
- /package/dist/{chunk-3R3YK7EM.js.map → chunk-JHZUFBUV.js.map} +0 -0
- /package/dist/{watch-GVZESOCE.js.map → watch-XCWADLPU.js.map} +0 -0
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
sanitizeHref,
|
|
26
26
|
stripDtd,
|
|
27
27
|
toArrayBuffer
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-JHZUFBUV.js";
|
|
29
29
|
import {
|
|
30
30
|
parsePageRange
|
|
31
31
|
} from "./chunk-MOL7MDBG.js";
|
|
@@ -34,9 +34,7 @@ import {
|
|
|
34
34
|
import { readFile } from "fs/promises";
|
|
35
35
|
|
|
36
36
|
// src/hwpx/parser.ts
|
|
37
|
-
import
|
|
38
|
-
import { inflateRawSync } from "zlib";
|
|
39
|
-
import { DOMParser } from "@xmldom/xmldom";
|
|
37
|
+
import JSZip2 from "jszip";
|
|
40
38
|
|
|
41
39
|
// src/hwpx/com-fallback.ts
|
|
42
40
|
import { execFileSync } from "child_process";
|
|
@@ -145,6 +143,245 @@ function comResultToParseResult(pages, pageCount, warnings) {
|
|
|
145
143
|
};
|
|
146
144
|
}
|
|
147
145
|
|
|
146
|
+
// src/hwpx/parser-shared.ts
|
|
147
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
148
|
+
var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
|
|
149
|
+
var MAX_ZIP_ENTRIES = 500;
|
|
150
|
+
function clampSpan(val, max) {
|
|
151
|
+
return Math.max(1, Math.min(val, max));
|
|
152
|
+
}
|
|
153
|
+
var MAX_XML_DEPTH = 200;
|
|
154
|
+
function createSectionShared() {
|
|
155
|
+
return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
|
|
156
|
+
}
|
|
157
|
+
function createXmlParser(warnings) {
|
|
158
|
+
return new DOMParser({
|
|
159
|
+
onError(level, msg2) {
|
|
160
|
+
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
|
|
161
|
+
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function applyPageText(blocks, shared) {
|
|
166
|
+
const { headers, footers } = shared.pageText;
|
|
167
|
+
if (headers.length > 0) {
|
|
168
|
+
blocks.unshift(...headers.map((t) => ({ type: "paragraph", text: t, pageNumber: 1 })));
|
|
169
|
+
}
|
|
170
|
+
if (footers.length > 0) {
|
|
171
|
+
blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function findChildByLocalName(parent, name) {
|
|
175
|
+
const children = parent.childNodes;
|
|
176
|
+
if (!children) return null;
|
|
177
|
+
for (let i = 0; i < children.length; i++) {
|
|
178
|
+
const ch = children[i];
|
|
179
|
+
if (ch.nodeType !== 1) continue;
|
|
180
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
181
|
+
if (tag === name) return ch;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function extractTextFromNode(node) {
|
|
186
|
+
let result = "";
|
|
187
|
+
const children = node.childNodes;
|
|
188
|
+
if (!children) return result;
|
|
189
|
+
for (let i = 0; i < children.length; i++) {
|
|
190
|
+
const child = children[i];
|
|
191
|
+
if (child.nodeType === 3) result += child.textContent || "";
|
|
192
|
+
else if (child.nodeType === 1) result += extractTextFromNode(child);
|
|
193
|
+
}
|
|
194
|
+
return result.trim();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/hwpx/styles.ts
|
|
198
|
+
async function extractHwpxStyles(zip, decompressed) {
|
|
199
|
+
const result = {
|
|
200
|
+
charProperties: /* @__PURE__ */ new Map(),
|
|
201
|
+
styles: /* @__PURE__ */ new Map(),
|
|
202
|
+
numberings: /* @__PURE__ */ new Map(),
|
|
203
|
+
bullets: /* @__PURE__ */ new Map(),
|
|
204
|
+
paraHeadings: /* @__PURE__ */ new Map()
|
|
205
|
+
};
|
|
206
|
+
const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
|
|
207
|
+
for (const hp of headerPaths) {
|
|
208
|
+
const hpLower = hp.toLowerCase();
|
|
209
|
+
const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
|
|
210
|
+
if (!file) continue;
|
|
211
|
+
try {
|
|
212
|
+
const xml = await file.async("text");
|
|
213
|
+
if (decompressed) {
|
|
214
|
+
decompressed.total += xml.length * 2;
|
|
215
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
216
|
+
}
|
|
217
|
+
const parser = createXmlParser();
|
|
218
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
219
|
+
if (!doc.documentElement) continue;
|
|
220
|
+
parseCharProperties(doc, result.charProperties);
|
|
221
|
+
parseStyleElements(doc, result.styles);
|
|
222
|
+
const domDoc = doc;
|
|
223
|
+
parseNumberings(domDoc, result.numberings);
|
|
224
|
+
parseBullets(domDoc, result.bullets);
|
|
225
|
+
parseParaHeadings(domDoc, result.paraHeadings);
|
|
226
|
+
break;
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function parseCharProperties(doc, map) {
|
|
234
|
+
const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
|
|
235
|
+
for (const tagName of tagNames) {
|
|
236
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
237
|
+
for (let i = 0; i < elements.length; i++) {
|
|
238
|
+
const el = elements[i];
|
|
239
|
+
const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
|
|
240
|
+
if (!id) continue;
|
|
241
|
+
const prop = {};
|
|
242
|
+
const height = el.getAttribute("height");
|
|
243
|
+
if (height) {
|
|
244
|
+
const parsedHeight = parseInt(height, 10);
|
|
245
|
+
if (!isNaN(parsedHeight) && parsedHeight > 0) {
|
|
246
|
+
prop.fontSize = parsedHeight / 100;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const bold = el.getAttribute("bold");
|
|
250
|
+
if (bold === "true" || bold === "1") prop.bold = true;
|
|
251
|
+
const italic = el.getAttribute("italic");
|
|
252
|
+
if (italic === "true" || italic === "1") prop.italic = true;
|
|
253
|
+
const fontFaces = el.getElementsByTagName("*");
|
|
254
|
+
for (let j = 0; j < fontFaces.length; j++) {
|
|
255
|
+
const ff = fontFaces[j];
|
|
256
|
+
const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
|
|
257
|
+
if (localTag === "fontface" || localTag === "fontRef") {
|
|
258
|
+
const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
|
|
259
|
+
if (face) {
|
|
260
|
+
prop.fontName = face;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
map.set(id, prop);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function parseStyleElements(doc, map) {
|
|
270
|
+
const tagNames = ["hh:style", "style", "hp:style"];
|
|
271
|
+
for (const tagName of tagNames) {
|
|
272
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
273
|
+
for (let i = 0; i < elements.length; i++) {
|
|
274
|
+
const el = elements[i];
|
|
275
|
+
const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
|
|
276
|
+
const name = el.getAttribute("name") || el.getAttribute("engName") || "";
|
|
277
|
+
const charPrId = el.getAttribute("charPrIDRef") || void 0;
|
|
278
|
+
const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
|
|
279
|
+
map.set(id, { name, charPrId, paraPrId });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function parseNumberings(doc, map) {
|
|
284
|
+
const tagNames = ["hh:numbering", "numbering"];
|
|
285
|
+
for (const tagName of tagNames) {
|
|
286
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
287
|
+
for (let i = 0; i < elements.length; i++) {
|
|
288
|
+
const el = elements[i];
|
|
289
|
+
const id = el.getAttribute("id") || "";
|
|
290
|
+
if (!id) continue;
|
|
291
|
+
const def = { heads: /* @__PURE__ */ new Map() };
|
|
292
|
+
const children = el.childNodes;
|
|
293
|
+
for (let j = 0; j < children.length; j++) {
|
|
294
|
+
const ch = children[j];
|
|
295
|
+
if (ch.nodeType !== 1) continue;
|
|
296
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
297
|
+
if (tag !== "paraHead") continue;
|
|
298
|
+
const level = parseInt(ch.getAttribute("level") || "", 10);
|
|
299
|
+
if (isNaN(level) || level < 1 || level > 10) continue;
|
|
300
|
+
const start = parseInt(ch.getAttribute("start") || "1", 10);
|
|
301
|
+
def.heads.set(level, {
|
|
302
|
+
numFormat: ch.getAttribute("numFormat") || "DIGIT",
|
|
303
|
+
text: ch.textContent || "",
|
|
304
|
+
start: isNaN(start) ? 1 : start
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (def.heads.size > 0) map.set(id, def);
|
|
308
|
+
}
|
|
309
|
+
if (map.size > 0) break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function parseBullets(doc, map) {
|
|
313
|
+
const tagNames = ["hh:bullet", "bullet"];
|
|
314
|
+
for (const tagName of tagNames) {
|
|
315
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
316
|
+
for (let i = 0; i < elements.length; i++) {
|
|
317
|
+
const el = elements[i];
|
|
318
|
+
const id = el.getAttribute("id") || "";
|
|
319
|
+
const char = el.getAttribute("char") || "";
|
|
320
|
+
if (id && char) map.set(id, char);
|
|
321
|
+
}
|
|
322
|
+
if (map.size > 0) break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function parseParaHeadings(doc, map) {
|
|
326
|
+
const tagNames = ["hh:paraPr", "paraPr"];
|
|
327
|
+
for (const tagName of tagNames) {
|
|
328
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
329
|
+
for (let i = 0; i < elements.length; i++) {
|
|
330
|
+
const el = elements[i];
|
|
331
|
+
const id = el.getAttribute("id") || "";
|
|
332
|
+
if (!id) continue;
|
|
333
|
+
const heading = findChildByLocalName(el, "heading");
|
|
334
|
+
if (!heading) continue;
|
|
335
|
+
const type = heading.getAttribute("type") || "NONE";
|
|
336
|
+
if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
|
|
337
|
+
const level = parseInt(heading.getAttribute("level") || "0", 10);
|
|
338
|
+
map.set(id, {
|
|
339
|
+
type,
|
|
340
|
+
idRef: heading.getAttribute("idRef") || "0",
|
|
341
|
+
level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (map.size > 0) break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function detectHwpxHeadings(blocks, styleMap) {
|
|
348
|
+
if (blocks.some((b) => b.type === "heading")) return;
|
|
349
|
+
let baseFontSize = 0;
|
|
350
|
+
const sizeFreq = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const b of blocks) {
|
|
352
|
+
if (b.style?.fontSize) {
|
|
353
|
+
sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let maxCount = 0;
|
|
357
|
+
for (const [size, count] of sizeFreq) {
|
|
358
|
+
if (count > maxCount) {
|
|
359
|
+
maxCount = count;
|
|
360
|
+
baseFontSize = size;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const block of blocks) {
|
|
364
|
+
if (block.type !== "paragraph" || !block.text) continue;
|
|
365
|
+
const text = block.text.trim();
|
|
366
|
+
if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
|
|
367
|
+
let level = 0;
|
|
368
|
+
if (baseFontSize > 0 && block.style?.fontSize) {
|
|
369
|
+
const ratio = block.style.fontSize / baseFontSize;
|
|
370
|
+
if (ratio >= HEADING_RATIO_H1) level = 1;
|
|
371
|
+
else if (ratio >= HEADING_RATIO_H2) level = 2;
|
|
372
|
+
else if (ratio >= HEADING_RATIO_H3) level = 3;
|
|
373
|
+
}
|
|
374
|
+
const compactText = text.replace(/\s+/g, "");
|
|
375
|
+
if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
|
|
376
|
+
if (level === 0) level = 3;
|
|
377
|
+
}
|
|
378
|
+
if (level > 0) {
|
|
379
|
+
block.type = "heading";
|
|
380
|
+
block.level = level;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
148
385
|
// src/hwpx/equation.ts
|
|
149
386
|
var CONVERT_MAP = {
|
|
150
387
|
TIMES: "\\times",
|
|
@@ -584,173 +821,7 @@ function hmlToLatex(hmlEqStr) {
|
|
|
584
821
|
return out;
|
|
585
822
|
}
|
|
586
823
|
|
|
587
|
-
// src/hwpx/
|
|
588
|
-
var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
|
|
589
|
-
var MAX_ZIP_ENTRIES = 500;
|
|
590
|
-
function clampSpan(val, max) {
|
|
591
|
-
return Math.max(1, Math.min(val, max));
|
|
592
|
-
}
|
|
593
|
-
var MAX_XML_DEPTH = 200;
|
|
594
|
-
function createSectionShared() {
|
|
595
|
-
return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
|
|
596
|
-
}
|
|
597
|
-
function createXmlParser(warnings) {
|
|
598
|
-
return new DOMParser({
|
|
599
|
-
onError(level, msg2) {
|
|
600
|
-
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
|
|
601
|
-
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
async function extractHwpxStyles(zip, decompressed) {
|
|
606
|
-
const result = {
|
|
607
|
-
charProperties: /* @__PURE__ */ new Map(),
|
|
608
|
-
styles: /* @__PURE__ */ new Map(),
|
|
609
|
-
numberings: /* @__PURE__ */ new Map(),
|
|
610
|
-
bullets: /* @__PURE__ */ new Map(),
|
|
611
|
-
paraHeadings: /* @__PURE__ */ new Map()
|
|
612
|
-
};
|
|
613
|
-
const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
|
|
614
|
-
for (const hp of headerPaths) {
|
|
615
|
-
const hpLower = hp.toLowerCase();
|
|
616
|
-
const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
|
|
617
|
-
if (!file) continue;
|
|
618
|
-
try {
|
|
619
|
-
const xml = await file.async("text");
|
|
620
|
-
if (decompressed) {
|
|
621
|
-
decompressed.total += xml.length * 2;
|
|
622
|
-
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
623
|
-
}
|
|
624
|
-
const parser = createXmlParser();
|
|
625
|
-
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
626
|
-
if (!doc.documentElement) continue;
|
|
627
|
-
parseCharProperties(doc, result.charProperties);
|
|
628
|
-
parseStyleElements(doc, result.styles);
|
|
629
|
-
const domDoc = doc;
|
|
630
|
-
parseNumberings(domDoc, result.numberings);
|
|
631
|
-
parseBullets(domDoc, result.bullets);
|
|
632
|
-
parseParaHeadings(domDoc, result.paraHeadings);
|
|
633
|
-
break;
|
|
634
|
-
} catch {
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
function parseCharProperties(doc, map) {
|
|
641
|
-
const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
|
|
642
|
-
for (const tagName of tagNames) {
|
|
643
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
644
|
-
for (let i = 0; i < elements.length; i++) {
|
|
645
|
-
const el = elements[i];
|
|
646
|
-
const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
|
|
647
|
-
if (!id) continue;
|
|
648
|
-
const prop = {};
|
|
649
|
-
const height = el.getAttribute("height");
|
|
650
|
-
if (height) {
|
|
651
|
-
const parsedHeight = parseInt(height, 10);
|
|
652
|
-
if (!isNaN(parsedHeight) && parsedHeight > 0) {
|
|
653
|
-
prop.fontSize = parsedHeight / 100;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
const bold = el.getAttribute("bold");
|
|
657
|
-
if (bold === "true" || bold === "1") prop.bold = true;
|
|
658
|
-
const italic = el.getAttribute("italic");
|
|
659
|
-
if (italic === "true" || italic === "1") prop.italic = true;
|
|
660
|
-
const fontFaces = el.getElementsByTagName("*");
|
|
661
|
-
for (let j = 0; j < fontFaces.length; j++) {
|
|
662
|
-
const ff = fontFaces[j];
|
|
663
|
-
const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
|
|
664
|
-
if (localTag === "fontface" || localTag === "fontRef") {
|
|
665
|
-
const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
|
|
666
|
-
if (face) {
|
|
667
|
-
prop.fontName = face;
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
map.set(id, prop);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
function parseStyleElements(doc, map) {
|
|
677
|
-
const tagNames = ["hh:style", "style", "hp:style"];
|
|
678
|
-
for (const tagName of tagNames) {
|
|
679
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
680
|
-
for (let i = 0; i < elements.length; i++) {
|
|
681
|
-
const el = elements[i];
|
|
682
|
-
const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
|
|
683
|
-
const name = el.getAttribute("name") || el.getAttribute("engName") || "";
|
|
684
|
-
const charPrId = el.getAttribute("charPrIDRef") || void 0;
|
|
685
|
-
const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
|
|
686
|
-
map.set(id, { name, charPrId, paraPrId });
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
function parseNumberings(doc, map) {
|
|
691
|
-
const tagNames = ["hh:numbering", "numbering"];
|
|
692
|
-
for (const tagName of tagNames) {
|
|
693
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
694
|
-
for (let i = 0; i < elements.length; i++) {
|
|
695
|
-
const el = elements[i];
|
|
696
|
-
const id = el.getAttribute("id") || "";
|
|
697
|
-
if (!id) continue;
|
|
698
|
-
const def = { heads: /* @__PURE__ */ new Map() };
|
|
699
|
-
const children = el.childNodes;
|
|
700
|
-
for (let j = 0; j < children.length; j++) {
|
|
701
|
-
const ch = children[j];
|
|
702
|
-
if (ch.nodeType !== 1) continue;
|
|
703
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
704
|
-
if (tag !== "paraHead") continue;
|
|
705
|
-
const level = parseInt(ch.getAttribute("level") || "", 10);
|
|
706
|
-
if (isNaN(level) || level < 1 || level > 10) continue;
|
|
707
|
-
const start = parseInt(ch.getAttribute("start") || "1", 10);
|
|
708
|
-
def.heads.set(level, {
|
|
709
|
-
numFormat: ch.getAttribute("numFormat") || "DIGIT",
|
|
710
|
-
text: ch.textContent || "",
|
|
711
|
-
start: isNaN(start) ? 1 : start
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
if (def.heads.size > 0) map.set(id, def);
|
|
715
|
-
}
|
|
716
|
-
if (map.size > 0) break;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
function parseBullets(doc, map) {
|
|
720
|
-
const tagNames = ["hh:bullet", "bullet"];
|
|
721
|
-
for (const tagName of tagNames) {
|
|
722
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
723
|
-
for (let i = 0; i < elements.length; i++) {
|
|
724
|
-
const el = elements[i];
|
|
725
|
-
const id = el.getAttribute("id") || "";
|
|
726
|
-
const char = el.getAttribute("char") || "";
|
|
727
|
-
if (id && char) map.set(id, char);
|
|
728
|
-
}
|
|
729
|
-
if (map.size > 0) break;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
function parseParaHeadings(doc, map) {
|
|
733
|
-
const tagNames = ["hh:paraPr", "paraPr"];
|
|
734
|
-
for (const tagName of tagNames) {
|
|
735
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
736
|
-
for (let i = 0; i < elements.length; i++) {
|
|
737
|
-
const el = elements[i];
|
|
738
|
-
const id = el.getAttribute("id") || "";
|
|
739
|
-
if (!id) continue;
|
|
740
|
-
const heading = findChildByLocalName(el, "heading");
|
|
741
|
-
if (!heading) continue;
|
|
742
|
-
const type = heading.getAttribute("type") || "NONE";
|
|
743
|
-
if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
|
|
744
|
-
const level = parseInt(heading.getAttribute("level") || "0", 10);
|
|
745
|
-
map.set(id, {
|
|
746
|
-
type,
|
|
747
|
-
idRef: heading.getAttribute("idRef") || "0",
|
|
748
|
-
level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
if (map.size > 0) break;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
824
|
+
// src/hwpx/para-heading.ts
|
|
754
825
|
var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
|
|
755
826
|
var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
|
|
756
827
|
function toRoman(n) {
|
|
@@ -843,1038 +914,986 @@ function resolveParaHeading(paraEl, ctx) {
|
|
|
843
914
|
});
|
|
844
915
|
return { prefix, headingLevel };
|
|
845
916
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (pages.some((p) => p && p.trim().length > 0)) {
|
|
865
|
-
return comResultToParseResult(pages, pageCount, warnings2);
|
|
917
|
+
|
|
918
|
+
// src/hwpx/table-build.ts
|
|
919
|
+
function buildTableWithCellMeta(state) {
|
|
920
|
+
const table = buildTable(state.rows);
|
|
921
|
+
if (state.caption) table.caption = state.caption;
|
|
922
|
+
const anchors = [];
|
|
923
|
+
{
|
|
924
|
+
const covered = /* @__PURE__ */ new Set();
|
|
925
|
+
for (let r = 0; r < table.rows; r++) {
|
|
926
|
+
for (let c = 0; c < table.cols; c++) {
|
|
927
|
+
if (covered.has(`${r},${c}`)) continue;
|
|
928
|
+
const cell = table.cells[r]?.[c];
|
|
929
|
+
if (!cell) continue;
|
|
930
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
931
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
932
|
+
if (dr === 0 && dc === 0) continue;
|
|
933
|
+
if (r + dr < table.rows && c + dc < table.cols) covered.add(`${r + dr},${c + dc}`);
|
|
934
|
+
}
|
|
866
935
|
}
|
|
936
|
+
anchors.push(cell);
|
|
937
|
+
c += cell.colSpan - 1;
|
|
867
938
|
}
|
|
868
|
-
throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
|
|
869
939
|
}
|
|
870
940
|
}
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (
|
|
897
|
-
|
|
941
|
+
const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
|
|
942
|
+
const ordinalReliable = anchors.length === srcCount;
|
|
943
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
944
|
+
let flatIdx = -1;
|
|
945
|
+
for (const row of state.rows) {
|
|
946
|
+
for (const src of row) {
|
|
947
|
+
flatIdx++;
|
|
948
|
+
const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
|
|
949
|
+
if (!needsBlocks && !src.isHeader) continue;
|
|
950
|
+
let target;
|
|
951
|
+
const trimmed = src.text.trim();
|
|
952
|
+
if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
|
|
953
|
+
const cand = table.cells[src.rowAddr]?.[src.colAddr];
|
|
954
|
+
if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
|
|
955
|
+
}
|
|
956
|
+
if (!target) {
|
|
957
|
+
outer: for (const irRow of table.cells) {
|
|
958
|
+
for (const cand of irRow) {
|
|
959
|
+
if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
|
|
960
|
+
target = cand;
|
|
961
|
+
break outer;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (!target && ordinalReliable) {
|
|
967
|
+
const cand = anchors[flatIdx];
|
|
968
|
+
if (cand && !claimed.has(cand)) target = cand;
|
|
969
|
+
}
|
|
970
|
+
if (!target) continue;
|
|
971
|
+
claimed.add(target);
|
|
972
|
+
if (needsBlocks) target.blocks = src.blocks;
|
|
973
|
+
if (src.isHeader) target.isHeader = true;
|
|
898
974
|
}
|
|
899
975
|
}
|
|
900
|
-
|
|
901
|
-
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
902
|
-
detectHwpxHeadings(blocks, styleMap);
|
|
903
|
-
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
904
|
-
const markdown = blocksToMarkdown(blocks);
|
|
905
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
|
|
976
|
+
return table;
|
|
906
977
|
}
|
|
907
|
-
function
|
|
908
|
-
const
|
|
909
|
-
if (
|
|
910
|
-
|
|
978
|
+
function completeTable(newTable, tableStack, blocks, ctx) {
|
|
979
|
+
const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
|
|
980
|
+
if (newTable.rows.length === 0) {
|
|
981
|
+
if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
|
|
982
|
+
return parentTable;
|
|
911
983
|
}
|
|
912
|
-
|
|
913
|
-
|
|
984
|
+
const ir = buildTableWithCellMeta(newTable);
|
|
985
|
+
const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
|
|
986
|
+
if (parentTable?.cell) {
|
|
987
|
+
const cell = parentTable.cell;
|
|
988
|
+
(cell.blocks ??= []).push(block);
|
|
989
|
+
cell.hasStructure = true;
|
|
990
|
+
let flat = convertTableToText(newTable.rows);
|
|
991
|
+
if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
|
|
992
|
+
if (flat) cell.text += (cell.text ? "\n" : "") + flat;
|
|
993
|
+
} else {
|
|
994
|
+
blocks.push(block);
|
|
914
995
|
}
|
|
996
|
+
return parentTable;
|
|
915
997
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
return "image/wmf";
|
|
932
|
-
case "emf":
|
|
933
|
-
return "image/emf";
|
|
934
|
-
case "svg":
|
|
935
|
-
return "image/svg+xml";
|
|
936
|
-
default:
|
|
937
|
-
return "application/octet-stream";
|
|
998
|
+
|
|
999
|
+
// src/hwpx/section-walker.ts
|
|
1000
|
+
function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
|
|
1001
|
+
const parser = createXmlParser(warnings);
|
|
1002
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1003
|
+
if (!doc.documentElement) return [];
|
|
1004
|
+
const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
|
|
1005
|
+
ctx.shared.track.deleteDepth = 0;
|
|
1006
|
+
for (const tagName of ["hp:secPr", "secPr"]) {
|
|
1007
|
+
const els = doc.getElementsByTagName(tagName);
|
|
1008
|
+
if (els.length > 0) {
|
|
1009
|
+
const v = els[0].getAttribute("outlineShapeIDRef");
|
|
1010
|
+
if (v) ctx.outlineNumId = v;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
938
1013
|
}
|
|
1014
|
+
const blocks = [];
|
|
1015
|
+
walkSection(doc.documentElement, blocks, null, [], ctx);
|
|
1016
|
+
return blocks;
|
|
939
1017
|
}
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
|
|
952
|
-
if (depth > MAX_XML_DEPTH) return;
|
|
953
|
-
for (const block of blocks) {
|
|
954
|
-
if (block.type === "image") {
|
|
955
|
-
out.push({ block, ownerCell });
|
|
956
|
-
} else if (block.type === "table" && block.table) {
|
|
957
|
-
for (const row of block.table.cells) {
|
|
958
|
-
for (const cell of row) {
|
|
959
|
-
if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
1018
|
+
function extractImageRef(el) {
|
|
1019
|
+
const children = el.childNodes;
|
|
1020
|
+
if (!children) return null;
|
|
1021
|
+
for (let i = 0; i < children.length; i++) {
|
|
1022
|
+
const child = children[i];
|
|
1023
|
+
if (child.nodeType !== 1) continue;
|
|
1024
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1025
|
+
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
1026
|
+
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
1027
|
+
if (ref) return ref;
|
|
962
1028
|
}
|
|
1029
|
+
const nested = extractImageRef(child);
|
|
1030
|
+
if (nested) return nested;
|
|
963
1031
|
}
|
|
1032
|
+
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
1033
|
+
if (directRef) return directRef;
|
|
1034
|
+
return null;
|
|
964
1035
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1036
|
+
function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1037
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
1038
|
+
const children = node.childNodes;
|
|
1039
|
+
if (!children) return;
|
|
1040
|
+
for (let i = 0; i < children.length; i++) {
|
|
1041
|
+
const el = children[i];
|
|
1042
|
+
if (el.nodeType !== 1) continue;
|
|
1043
|
+
const tag = el.tagName || el.localName || "";
|
|
1044
|
+
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1045
|
+
switch (localTag) {
|
|
1046
|
+
case "tbl": {
|
|
1047
|
+
if (tableCtx) tableStack.push(tableCtx);
|
|
1048
|
+
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1049
|
+
walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
|
|
1050
|
+
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
// 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
|
|
1054
|
+
case "caption":
|
|
1055
|
+
if (tableCtx) {
|
|
1056
|
+
const capText = collectSubListText(el, ctx);
|
|
1057
|
+
if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
|
|
1058
|
+
}
|
|
1059
|
+
break;
|
|
1060
|
+
case "tr":
|
|
1061
|
+
if (tableCtx) {
|
|
1062
|
+
tableCtx.currentRow = [];
|
|
1063
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1064
|
+
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
1065
|
+
tableCtx.currentRow = [];
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
1068
|
+
case "tc":
|
|
1069
|
+
if (tableCtx) {
|
|
1070
|
+
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1071
|
+
if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
|
|
1072
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1073
|
+
if (tableCtx.cell) {
|
|
1074
|
+
tableCtx.currentRow.push(tableCtx.cell);
|
|
1075
|
+
tableCtx.cell = null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
break;
|
|
1079
|
+
case "cellAddr":
|
|
1080
|
+
if (tableCtx?.cell) {
|
|
1081
|
+
const ca = parseInt(el.getAttribute("colAddr") || "", 10);
|
|
1082
|
+
const ra = parseInt(el.getAttribute("rowAddr") || "", 10);
|
|
1083
|
+
if (!isNaN(ca)) tableCtx.cell.colAddr = ca;
|
|
1084
|
+
if (!isNaN(ra)) tableCtx.cell.rowAddr = ra;
|
|
1085
|
+
}
|
|
1086
|
+
break;
|
|
1087
|
+
case "cellSpan":
|
|
1088
|
+
if (tableCtx?.cell) {
|
|
1089
|
+
const rawCs = parseInt(el.getAttribute("colSpan") || "1", 10);
|
|
1090
|
+
const cs = isNaN(rawCs) ? 1 : rawCs;
|
|
1091
|
+
const rawRs = parseInt(el.getAttribute("rowSpan") || "1", 10);
|
|
1092
|
+
const rs = isNaN(rawRs) ? 1 : rawRs;
|
|
1093
|
+
tableCtx.cell.colSpan = clampSpan(cs, MAX_COLS);
|
|
1094
|
+
tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
|
|
1095
|
+
}
|
|
1096
|
+
break;
|
|
1097
|
+
case "p": {
|
|
1098
|
+
const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
|
|
1099
|
+
let text = rawText;
|
|
1100
|
+
let headingLevel;
|
|
1101
|
+
if (text) {
|
|
1102
|
+
const ph = resolveParaHeading(el, ctx);
|
|
1103
|
+
if (ph?.prefix) text = ph.prefix + " " + text;
|
|
1104
|
+
headingLevel = ph?.headingLevel;
|
|
1105
|
+
}
|
|
1106
|
+
if (text) {
|
|
1107
|
+
if (tableCtx?.cell) {
|
|
1108
|
+
const cell = tableCtx.cell;
|
|
1109
|
+
if (footnote) text += ` (\uC8FC: ${footnote})`;
|
|
1110
|
+
cell.text += (cell.text ? "\n" : "") + text;
|
|
1111
|
+
(cell.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
|
|
1112
|
+
} else if (!tableCtx) {
|
|
1113
|
+
const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
|
|
1114
|
+
if (headingLevel) block.level = headingLevel;
|
|
1115
|
+
if (style) block.style = style;
|
|
1116
|
+
if (href) block.href = href;
|
|
1117
|
+
if (footnote) block.footnoteText = footnote;
|
|
1118
|
+
blocks.push(block);
|
|
1119
|
+
} else {
|
|
1120
|
+
blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
|
|
991
1121
|
}
|
|
992
1122
|
}
|
|
1123
|
+
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1124
|
+
break;
|
|
993
1125
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const mimeType = imageExtToMime(ext);
|
|
1005
|
-
imageIndex++;
|
|
1006
|
-
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
1007
|
-
img = { filename, data, mimeType };
|
|
1008
|
-
images.push(img);
|
|
1009
|
-
break;
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
if (err instanceof KordocError) throw err;
|
|
1126
|
+
// 이미지/그림/글상자 — 이미지·텍스트·캡션 병행 추출
|
|
1127
|
+
case "pic":
|
|
1128
|
+
case "shape":
|
|
1129
|
+
case "drawingObject": {
|
|
1130
|
+
if (tableCtx?.cell) {
|
|
1131
|
+
const sink = [];
|
|
1132
|
+
handleShape(el, sink, ctx);
|
|
1133
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1134
|
+
} else {
|
|
1135
|
+
handleShape(el, blocks, ctx);
|
|
1012
1136
|
}
|
|
1137
|
+
break;
|
|
1013
1138
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
block.text = img.filename;
|
|
1024
|
-
block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
|
|
1025
|
-
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, ``);
|
|
1026
|
-
}
|
|
1027
|
-
return images;
|
|
1028
|
-
}
|
|
1029
|
-
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
1030
|
-
try {
|
|
1031
|
-
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
1032
|
-
for (const mp of metaPaths) {
|
|
1033
|
-
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
1034
|
-
if (!file) continue;
|
|
1035
|
-
const xml = await file.async("text");
|
|
1036
|
-
if (decompressed) {
|
|
1037
|
-
decompressed.total += xml.length * 2;
|
|
1038
|
-
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1139
|
+
// 메모 — 본문 혼입 차단 (v3.0)
|
|
1140
|
+
case "memogroup":
|
|
1141
|
+
case "memo": {
|
|
1142
|
+
if (ctx.warnings && extractTextFromNode(el)) {
|
|
1143
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1039
1146
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1147
|
+
default:
|
|
1148
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1149
|
+
break;
|
|
1042
1150
|
}
|
|
1043
|
-
} catch {
|
|
1044
1151
|
}
|
|
1045
1152
|
}
|
|
1046
|
-
function
|
|
1047
|
-
const
|
|
1048
|
-
const
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const text = els[0].textContent?.trim();
|
|
1055
|
-
if (text) return text;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
return void 0;
|
|
1059
|
-
};
|
|
1060
|
-
metadata.title = metadata.title || getText(["dc:title", "title"]);
|
|
1061
|
-
metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
|
|
1062
|
-
metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
|
|
1063
|
-
metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
|
|
1064
|
-
metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
|
|
1065
|
-
const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
|
|
1066
|
-
if (keywords && !metadata.keywords) {
|
|
1067
|
-
metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
1153
|
+
function handleShape(el, sink, ctx) {
|
|
1154
|
+
const imgRef = extractImageRef(el);
|
|
1155
|
+
const drawTextChild = findDescendant(el, "drawText");
|
|
1156
|
+
if (imgRef) {
|
|
1157
|
+
const block = { type: "image", text: imgRef, pageNumber: ctx.sectionNum };
|
|
1158
|
+
const alt = userShapeComment(el);
|
|
1159
|
+
if (alt) block.footnoteText = alt;
|
|
1160
|
+
sink.push(block);
|
|
1068
1161
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1162
|
+
if (drawTextChild) {
|
|
1163
|
+
extractDrawTextBlocks(drawTextChild, sink, ctx);
|
|
1164
|
+
}
|
|
1165
|
+
const capEl = findChildByLocalName(el, "caption");
|
|
1166
|
+
if (capEl) {
|
|
1167
|
+
const capText = collectSubListText(capEl, ctx);
|
|
1168
|
+
if (capText) sink.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
|
|
1169
|
+
}
|
|
1170
|
+
if (!imgRef && !drawTextChild && ctx.warnings && ctx.sectionNum) {
|
|
1171
|
+
const localTag = (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
|
|
1172
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
1076
1173
|
}
|
|
1077
|
-
const metadata = {};
|
|
1078
|
-
await extractHwpxMetadata(zip, metadata);
|
|
1079
|
-
const sectionPaths = await resolveSectionPaths(zip);
|
|
1080
|
-
metadata.pageCount = sectionPaths.length;
|
|
1081
|
-
return metadata;
|
|
1082
1174
|
}
|
|
1083
|
-
function
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
pos++;
|
|
1175
|
+
function userShapeComment(el) {
|
|
1176
|
+
const commentEl = findChildByLocalName(el, "shapeComment");
|
|
1177
|
+
if (!commentEl) return void 0;
|
|
1178
|
+
const text = extractTextFromNode(commentEl);
|
|
1179
|
+
if (!text) return void 0;
|
|
1180
|
+
if (/^그림입니다/.test(text)) return void 0;
|
|
1181
|
+
if (/^(?:모서리가 둥근 |둥근 )?[^\n]{1,20}입니다\.?$/.test(text)) return void 0;
|
|
1182
|
+
return text;
|
|
1183
|
+
}
|
|
1184
|
+
function mergeBlocksIntoCell(cell, sink) {
|
|
1185
|
+
for (const b of sink) {
|
|
1186
|
+
if ((b.type === "paragraph" || b.type === "heading") && b.text) {
|
|
1187
|
+
cell.text += (cell.text ? "\n" : "") + b.text;
|
|
1188
|
+
(cell.blocks ??= []).push(b);
|
|
1189
|
+
} else if (b.type === "image" || b.type === "table") {
|
|
1190
|
+
if (b.type === "image" && b.text) {
|
|
1191
|
+
cell.text += (cell.text ? "\n" : "") + ``;
|
|
1101
1192
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const method = view.getUint16(pos + 8, true);
|
|
1106
|
-
const compSize = view.getUint32(pos + 18, true);
|
|
1107
|
-
const nameLen = view.getUint16(pos + 26, true);
|
|
1108
|
-
const extraLen = view.getUint16(pos + 28, true);
|
|
1109
|
-
if (nameLen > 1024 || extraLen > 65535) {
|
|
1110
|
-
pos += 30 + nameLen + extraLen;
|
|
1111
|
-
continue;
|
|
1193
|
+
;
|
|
1194
|
+
(cell.blocks ??= []).push(b);
|
|
1195
|
+
cell.hasStructure = true;
|
|
1112
1196
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function collectSubListText(el, ctx, depth = 0) {
|
|
1200
|
+
if (depth > 10) return "";
|
|
1201
|
+
const parts = [];
|
|
1202
|
+
const children = el.childNodes;
|
|
1203
|
+
if (!children) return "";
|
|
1204
|
+
for (let i = 0; i < children.length; i++) {
|
|
1205
|
+
const ch = children[i];
|
|
1206
|
+
if (ch.nodeType !== 1) continue;
|
|
1207
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1208
|
+
if (tag === "p" || tag === "para") {
|
|
1209
|
+
const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
|
|
1210
|
+
if (t) parts.push(t);
|
|
1211
|
+
} else if (tag === "tbl") {
|
|
1117
1212
|
continue;
|
|
1213
|
+
} else {
|
|
1214
|
+
const t = collectSubListText(ch, ctx, depth + 1);
|
|
1215
|
+
if (t) parts.push(t);
|
|
1118
1216
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1217
|
+
}
|
|
1218
|
+
return parts.join("\n").trim();
|
|
1219
|
+
}
|
|
1220
|
+
function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1221
|
+
if (depth > MAX_XML_DEPTH) return tableCtx;
|
|
1222
|
+
const children = node.childNodes;
|
|
1223
|
+
if (!children) return tableCtx;
|
|
1224
|
+
const walkChildren = (parent, d) => {
|
|
1225
|
+
if (d > MAX_XML_DEPTH) return;
|
|
1226
|
+
const kids2 = parent.childNodes;
|
|
1227
|
+
if (!kids2) return;
|
|
1228
|
+
for (let i = 0; i < kids2.length; i++) {
|
|
1229
|
+
const el = kids2[i];
|
|
1230
|
+
if (el.nodeType !== 1) continue;
|
|
1231
|
+
const tag = el.tagName || el.localName || "";
|
|
1232
|
+
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1233
|
+
if (localTag === "tbl") {
|
|
1234
|
+
if (tableCtx) tableStack.push(tableCtx);
|
|
1235
|
+
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1236
|
+
walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
|
|
1237
|
+
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1238
|
+
} else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
|
|
1239
|
+
if (tableCtx?.cell) {
|
|
1240
|
+
const sink = [];
|
|
1241
|
+
handleShape(el, sink, ctx);
|
|
1242
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1243
|
+
} else {
|
|
1244
|
+
handleShape(el, blocks, ctx);
|
|
1245
|
+
}
|
|
1246
|
+
} else if (localTag === "drawText") {
|
|
1247
|
+
if (tableCtx?.cell) {
|
|
1248
|
+
const sink = [];
|
|
1249
|
+
extractDrawTextBlocks(el, sink, ctx);
|
|
1250
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1251
|
+
} else {
|
|
1252
|
+
extractDrawTextBlocks(el, blocks, ctx);
|
|
1253
|
+
}
|
|
1254
|
+
} else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
|
|
1255
|
+
walkChildren(el, d + 1);
|
|
1256
|
+
}
|
|
1124
1257
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1258
|
+
};
|
|
1259
|
+
walkChildren(node, depth);
|
|
1260
|
+
return tableCtx;
|
|
1261
|
+
}
|
|
1262
|
+
function findDescendant(node, targetTag, depth = 0) {
|
|
1263
|
+
if (depth > 5) return null;
|
|
1264
|
+
const children = node.childNodes;
|
|
1265
|
+
if (!children) return null;
|
|
1266
|
+
for (let i = 0; i < children.length; i++) {
|
|
1267
|
+
const child = children[i];
|
|
1268
|
+
if (child.nodeType !== 1) continue;
|
|
1269
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1270
|
+
if (tag === targetTag) return child;
|
|
1271
|
+
const found = findDescendant(child, targetTag, depth + 1);
|
|
1272
|
+
if (found) return found;
|
|
1273
|
+
}
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
|
|
1277
|
+
const children = drawTextNode.childNodes;
|
|
1278
|
+
if (!children) return;
|
|
1279
|
+
for (let i = 0; i < children.length; i++) {
|
|
1280
|
+
const child = children[i];
|
|
1281
|
+
if (child.nodeType !== 1) continue;
|
|
1282
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1283
|
+
if (tag === "subList" || tag === "p" || tag === "para") {
|
|
1284
|
+
if (tag === "subList") {
|
|
1285
|
+
extractDrawTextBlocks(child, blocks, ctx);
|
|
1135
1286
|
} else {
|
|
1136
|
-
|
|
1287
|
+
const info = extractParagraphInfo(child, ctx.styleMap, ctx);
|
|
1288
|
+
let text = info.text.trim();
|
|
1289
|
+
if (text) {
|
|
1290
|
+
const ph = resolveParaHeading(child, ctx);
|
|
1291
|
+
if (ph?.prefix) text = ph.prefix + " " + text;
|
|
1292
|
+
const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
|
|
1293
|
+
if (info.href) block.href = info.href;
|
|
1294
|
+
if (info.footnote) block.footnoteText = info.footnote;
|
|
1295
|
+
blocks.push(block);
|
|
1296
|
+
}
|
|
1297
|
+
walkParagraphChildren(child, blocks, null, [], ctx);
|
|
1137
1298
|
}
|
|
1138
|
-
totalDecompressed += content.length * 2;
|
|
1139
|
-
if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
|
|
1140
|
-
sectionNum++;
|
|
1141
|
-
blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
|
|
1142
|
-
} catch {
|
|
1143
|
-
continue;
|
|
1144
1299
|
}
|
|
1145
1300
|
}
|
|
1146
|
-
if (blocks.length === 0) throw new KordocError("\uC190\uC0C1\uB41C HWPX\uC5D0\uC11C \uC139\uC158 \uB370\uC774\uD130\uB97C \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1147
|
-
applyPageText(blocks, shared);
|
|
1148
|
-
const markdown = blocksToMarkdown(blocks);
|
|
1149
|
-
return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1150
1301
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const items = doc.getElementsByTagName("opf:item");
|
|
1168
|
-
const spine = doc.getElementsByTagName("opf:itemref");
|
|
1169
|
-
const idToHref = /* @__PURE__ */ new Map();
|
|
1170
|
-
for (let i = 0; i < items.length; i++) {
|
|
1171
|
-
const item = items[i];
|
|
1172
|
-
const id = item.getAttribute("id") || "";
|
|
1173
|
-
const href = normalizeSectionHref(item.getAttribute("href") || "");
|
|
1174
|
-
if (id && href) idToHref.set(id, href);
|
|
1175
|
-
}
|
|
1176
|
-
if (spine.length > 0) {
|
|
1177
|
-
const ordered = [];
|
|
1178
|
-
for (let i = 0; i < spine.length; i++) {
|
|
1179
|
-
const href = idToHref.get(spine[i].getAttribute("idref") || "");
|
|
1180
|
-
if (href) ordered.push(href);
|
|
1181
|
-
}
|
|
1182
|
-
if (ordered.length > 0) return ordered;
|
|
1302
|
+
function extractHyperlinkHref(fieldBegin) {
|
|
1303
|
+
if ((fieldBegin.getAttribute("type") || "").toUpperCase() !== "HYPERLINK") return void 0;
|
|
1304
|
+
const params = findChildByLocalName(fieldBegin, "parameters");
|
|
1305
|
+
if (!params) return void 0;
|
|
1306
|
+
const children = params.childNodes;
|
|
1307
|
+
if (!children) return void 0;
|
|
1308
|
+
for (let i = 0; i < children.length; i++) {
|
|
1309
|
+
const ch = children[i];
|
|
1310
|
+
if (ch.nodeType !== 1) continue;
|
|
1311
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1312
|
+
if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
|
|
1313
|
+
let url = (ch.textContent || "").trim();
|
|
1314
|
+
if (!url) continue;
|
|
1315
|
+
url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
|
|
1316
|
+
const safe = sanitizeHref(url);
|
|
1317
|
+
if (safe) return safe;
|
|
1183
1318
|
}
|
|
1184
|
-
return
|
|
1319
|
+
return void 0;
|
|
1185
1320
|
}
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
let baseFontSize = 0;
|
|
1189
|
-
const sizeFreq = /* @__PURE__ */ new Map();
|
|
1190
|
-
for (const b of blocks) {
|
|
1191
|
-
if (b.style?.fontSize) {
|
|
1192
|
-
sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
let maxCount = 0;
|
|
1196
|
-
for (const [size, count] of sizeFreq) {
|
|
1197
|
-
if (count > maxCount) {
|
|
1198
|
-
maxCount = count;
|
|
1199
|
-
baseFontSize = size;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
for (const block of blocks) {
|
|
1203
|
-
if (block.type !== "paragraph" || !block.text) continue;
|
|
1204
|
-
const text = block.text.trim();
|
|
1205
|
-
if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
|
|
1206
|
-
let level = 0;
|
|
1207
|
-
if (baseFontSize > 0 && block.style?.fontSize) {
|
|
1208
|
-
const ratio = block.style.fontSize / baseFontSize;
|
|
1209
|
-
if (ratio >= HEADING_RATIO_H1) level = 1;
|
|
1210
|
-
else if (ratio >= HEADING_RATIO_H2) level = 2;
|
|
1211
|
-
else if (ratio >= HEADING_RATIO_H3) level = 3;
|
|
1212
|
-
}
|
|
1213
|
-
const compactText = text.replace(/\s+/g, "");
|
|
1214
|
-
if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
|
|
1215
|
-
if (level === 0) level = 3;
|
|
1216
|
-
}
|
|
1217
|
-
if (level > 0) {
|
|
1218
|
-
block.type = "heading";
|
|
1219
|
-
block.level = level;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1321
|
+
function isInDeletedRange(ctx) {
|
|
1322
|
+
return (ctx?.shared.track.deleteDepth ?? 0) > 0;
|
|
1222
1323
|
}
|
|
1223
|
-
function
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1324
|
+
function extractParagraphInfo(para, styleMap, ctx) {
|
|
1325
|
+
let text = "";
|
|
1326
|
+
let href;
|
|
1327
|
+
let footnote;
|
|
1328
|
+
let charPrId;
|
|
1329
|
+
const handleCtrl = (ctrlEl) => {
|
|
1330
|
+
const kids2 = ctrlEl.childNodes;
|
|
1331
|
+
if (!kids2) return;
|
|
1332
|
+
for (let j = 0; j < kids2.length; j++) {
|
|
1333
|
+
const k = kids2[j];
|
|
1334
|
+
if (k.nodeType !== 1) continue;
|
|
1335
|
+
const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
|
|
1336
|
+
switch (ktag) {
|
|
1337
|
+
// 머리말/꼬리말 — 문서당 1회 수집, 본문 앞/뒤 배치
|
|
1338
|
+
case "header":
|
|
1339
|
+
case "footer": {
|
|
1340
|
+
if (!ctx) break;
|
|
1341
|
+
const t = collectSubListText(k, ctx);
|
|
1342
|
+
if (t) {
|
|
1343
|
+
const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
|
|
1344
|
+
if (!bucket.includes(t)) bucket.push(t);
|
|
1345
|
+
}
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
// 각주/미주 — 해당 문단의 footnote로 인라인 보존
|
|
1349
|
+
case "footNote":
|
|
1350
|
+
case "endNote": {
|
|
1351
|
+
const noteText = extractTextFromNode(k);
|
|
1352
|
+
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
// 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
|
|
1356
|
+
case "fieldBegin": {
|
|
1357
|
+
const url = extractHyperlinkHref(k);
|
|
1358
|
+
if (url && !href) href = url;
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
case "fieldEnd":
|
|
1362
|
+
break;
|
|
1363
|
+
// 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
|
|
1364
|
+
case "deleteBegin":
|
|
1365
|
+
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1366
|
+
break;
|
|
1367
|
+
case "deleteEnd":
|
|
1368
|
+
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1369
|
+
break;
|
|
1370
|
+
case "insertBegin":
|
|
1371
|
+
case "insertEnd":
|
|
1372
|
+
break;
|
|
1373
|
+
// 삽입분은 최종본에 포함
|
|
1374
|
+
// 숨은 설명 — 본문 혼입 차단
|
|
1375
|
+
case "hiddenComment": {
|
|
1376
|
+
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1377
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
|
|
1378
|
+
}
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
// 콘텐츠 없는 제어 요소 — 스킵
|
|
1382
|
+
case "bookmark":
|
|
1383
|
+
case "pageNum":
|
|
1384
|
+
case "pageNumCtrl":
|
|
1385
|
+
case "pageHiding":
|
|
1386
|
+
case "newNum":
|
|
1387
|
+
case "autoNum":
|
|
1388
|
+
case "indexmark":
|
|
1389
|
+
case "colPr":
|
|
1390
|
+
break;
|
|
1391
|
+
// 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
|
|
1392
|
+
default: {
|
|
1393
|
+
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1394
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
|
|
1238
1395
|
}
|
|
1239
1396
|
}
|
|
1240
|
-
anchors.push(cell);
|
|
1241
|
-
c += cell.colSpan - 1;
|
|
1242
1397
|
}
|
|
1243
1398
|
}
|
|
1244
|
-
}
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
|
|
1257
|
-
const cand = table.cells[src.rowAddr]?.[src.colAddr];
|
|
1258
|
-
if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
|
|
1259
|
-
}
|
|
1260
|
-
if (!target) {
|
|
1261
|
-
outer: for (const irRow of table.cells) {
|
|
1262
|
-
for (const cand of irRow) {
|
|
1263
|
-
if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
|
|
1264
|
-
target = cand;
|
|
1265
|
-
break outer;
|
|
1266
|
-
}
|
|
1399
|
+
};
|
|
1400
|
+
const walk = (node) => {
|
|
1401
|
+
const children = node.childNodes;
|
|
1402
|
+
if (!children) return;
|
|
1403
|
+
for (let i = 0; i < children.length; i++) {
|
|
1404
|
+
const child = children[i];
|
|
1405
|
+
if (child.nodeType === 3) {
|
|
1406
|
+
const t = child.textContent || "";
|
|
1407
|
+
if (isInDeletedRange(ctx)) {
|
|
1408
|
+
if (t && ctx && !ctx.shared.track.warned) {
|
|
1409
|
+
ctx.shared.track.warned = true;
|
|
1410
|
+
ctx.warnings?.push({ page: ctx.sectionNum, message: "\uBCC0\uACBD\uCD94\uC801 \uC0AD\uC81C \uD14D\uC2A4\uD2B8 \uCD9C\uB825 \uC81C\uC678", code: "HIDDEN_TEXT_FILTERED" });
|
|
1267
1411
|
}
|
|
1412
|
+
} else {
|
|
1413
|
+
text += t;
|
|
1268
1414
|
}
|
|
1415
|
+
continue;
|
|
1269
1416
|
}
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
|
|
1284
|
-
if (newTable.rows.length === 0) {
|
|
1285
|
-
if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
|
|
1286
|
-
return parentTable;
|
|
1287
|
-
}
|
|
1288
|
-
const ir = buildTableWithCellMeta(newTable);
|
|
1289
|
-
const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
|
|
1290
|
-
if (parentTable?.cell) {
|
|
1291
|
-
const cell = parentTable.cell;
|
|
1292
|
-
(cell.blocks ??= []).push(block);
|
|
1293
|
-
cell.hasStructure = true;
|
|
1294
|
-
let flat = convertTableToText(newTable.rows);
|
|
1295
|
-
if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
|
|
1296
|
-
if (flat) cell.text += (cell.text ? "\n" : "") + flat;
|
|
1297
|
-
} else {
|
|
1298
|
-
blocks.push(block);
|
|
1299
|
-
}
|
|
1300
|
-
return parentTable;
|
|
1301
|
-
}
|
|
1302
|
-
function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
|
|
1303
|
-
const parser = createXmlParser(warnings);
|
|
1304
|
-
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1305
|
-
if (!doc.documentElement) return [];
|
|
1306
|
-
const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
|
|
1307
|
-
ctx.shared.track.deleteDepth = 0;
|
|
1308
|
-
for (const tagName of ["hp:secPr", "secPr"]) {
|
|
1309
|
-
const els = doc.getElementsByTagName(tagName);
|
|
1310
|
-
if (els.length > 0) {
|
|
1311
|
-
const v = els[0].getAttribute("outlineShapeIDRef");
|
|
1312
|
-
if (v) ctx.outlineNumId = v;
|
|
1313
|
-
break;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
const blocks = [];
|
|
1317
|
-
walkSection(doc.documentElement, blocks, null, [], ctx);
|
|
1318
|
-
return blocks;
|
|
1319
|
-
}
|
|
1320
|
-
function extractImageRef(el) {
|
|
1321
|
-
const children = el.childNodes;
|
|
1322
|
-
if (!children) return null;
|
|
1323
|
-
for (let i = 0; i < children.length; i++) {
|
|
1324
|
-
const child = children[i];
|
|
1325
|
-
if (child.nodeType !== 1) continue;
|
|
1326
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1327
|
-
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
1328
|
-
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
1329
|
-
if (ref) return ref;
|
|
1330
|
-
}
|
|
1331
|
-
const nested = extractImageRef(child);
|
|
1332
|
-
if (nested) return nested;
|
|
1333
|
-
}
|
|
1334
|
-
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
1335
|
-
if (directRef) return directRef;
|
|
1336
|
-
return null;
|
|
1337
|
-
}
|
|
1338
|
-
function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1339
|
-
if (depth > MAX_XML_DEPTH) return;
|
|
1340
|
-
const children = node.childNodes;
|
|
1341
|
-
if (!children) return;
|
|
1342
|
-
for (let i = 0; i < children.length; i++) {
|
|
1343
|
-
const el = children[i];
|
|
1344
|
-
if (el.nodeType !== 1) continue;
|
|
1345
|
-
const tag = el.tagName || el.localName || "";
|
|
1346
|
-
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1347
|
-
switch (localTag) {
|
|
1348
|
-
case "tbl": {
|
|
1349
|
-
if (tableCtx) tableStack.push(tableCtx);
|
|
1350
|
-
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1351
|
-
walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
|
|
1352
|
-
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1353
|
-
break;
|
|
1354
|
-
}
|
|
1355
|
-
// 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
|
|
1356
|
-
case "caption":
|
|
1357
|
-
if (tableCtx) {
|
|
1358
|
-
const capText = collectSubListText(el, ctx);
|
|
1359
|
-
if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
|
|
1360
|
-
}
|
|
1361
|
-
break;
|
|
1362
|
-
case "tr":
|
|
1363
|
-
if (tableCtx) {
|
|
1364
|
-
tableCtx.currentRow = [];
|
|
1365
|
-
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1366
|
-
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
1367
|
-
tableCtx.currentRow = [];
|
|
1368
|
-
}
|
|
1369
|
-
break;
|
|
1370
|
-
case "tc":
|
|
1371
|
-
if (tableCtx) {
|
|
1372
|
-
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1373
|
-
if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
|
|
1374
|
-
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1375
|
-
if (tableCtx.cell) {
|
|
1376
|
-
tableCtx.currentRow.push(tableCtx.cell);
|
|
1377
|
-
tableCtx.cell = null;
|
|
1417
|
+
if (child.nodeType !== 1) continue;
|
|
1418
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1419
|
+
switch (tag) {
|
|
1420
|
+
case "t":
|
|
1421
|
+
walk(child);
|
|
1422
|
+
break;
|
|
1423
|
+
// 자식 순회 (tab 등 하위 요소 처리)
|
|
1424
|
+
case "tab": {
|
|
1425
|
+
const leader = child.getAttribute("leader");
|
|
1426
|
+
if (leader && leader !== "0") {
|
|
1427
|
+
text += "";
|
|
1428
|
+
} else {
|
|
1429
|
+
text += " ";
|
|
1378
1430
|
}
|
|
1431
|
+
break;
|
|
1379
1432
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1433
|
+
case "br":
|
|
1434
|
+
if ((child.getAttribute("type") || "line") === "line") text += "\n";
|
|
1435
|
+
break;
|
|
1436
|
+
case "lineBreak":
|
|
1437
|
+
text += "\n";
|
|
1438
|
+
break;
|
|
1439
|
+
// 강제 줄바꿈 — ref 추출기·소스맵 스캐너와 동일 모델
|
|
1440
|
+
case "fwSpace":
|
|
1441
|
+
case "hwSpace":
|
|
1442
|
+
text += " ";
|
|
1443
|
+
break;
|
|
1444
|
+
case "tbl":
|
|
1445
|
+
break;
|
|
1446
|
+
// 테이블은 walkSection에서 처리
|
|
1447
|
+
// 하이퍼링크
|
|
1448
|
+
case "hyperlink": {
|
|
1449
|
+
const url = child.getAttribute("url") || child.getAttribute("href") || "";
|
|
1450
|
+
if (url) {
|
|
1451
|
+
const safe = sanitizeHref(url);
|
|
1452
|
+
if (safe) href = safe;
|
|
1453
|
+
}
|
|
1454
|
+
walk(child);
|
|
1455
|
+
break;
|
|
1387
1456
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
|
|
1457
|
+
// 각주/미주
|
|
1458
|
+
case "footNote":
|
|
1459
|
+
case "endNote":
|
|
1460
|
+
case "fn":
|
|
1461
|
+
case "en": {
|
|
1462
|
+
const noteText = extractTextFromNode(child);
|
|
1463
|
+
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1464
|
+
break;
|
|
1397
1465
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
const
|
|
1405
|
-
if (
|
|
1406
|
-
|
|
1466
|
+
// 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
|
|
1467
|
+
case "ctrl":
|
|
1468
|
+
handleCtrl(child);
|
|
1469
|
+
break;
|
|
1470
|
+
// run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
|
|
1471
|
+
case "fieldBegin": {
|
|
1472
|
+
const url = extractHyperlinkHref(child);
|
|
1473
|
+
if (url && !href) href = url;
|
|
1474
|
+
break;
|
|
1407
1475
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1476
|
+
// run 직계 변경추적 마커 (비표준 경로)
|
|
1477
|
+
case "deleteBegin":
|
|
1478
|
+
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1479
|
+
break;
|
|
1480
|
+
case "deleteEnd":
|
|
1481
|
+
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1482
|
+
break;
|
|
1483
|
+
case "insertBegin":
|
|
1484
|
+
case "insertEnd":
|
|
1485
|
+
break;
|
|
1486
|
+
case "fieldEnd":
|
|
1487
|
+
case "parameters":
|
|
1488
|
+
case "stringParam":
|
|
1489
|
+
case "integerParam":
|
|
1490
|
+
case "boolParam":
|
|
1491
|
+
case "floatParam":
|
|
1492
|
+
case "secPr":
|
|
1493
|
+
// 섹션 속성 (페이지 설정 등)
|
|
1494
|
+
case "colPr":
|
|
1495
|
+
// 다단 속성
|
|
1496
|
+
case "linesegarray":
|
|
1497
|
+
case "lineseg":
|
|
1498
|
+
// 레이아웃 정보
|
|
1499
|
+
// 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
|
|
1500
|
+
case "pic":
|
|
1501
|
+
case "shape":
|
|
1502
|
+
case "drawingObject":
|
|
1503
|
+
case "shapeComment":
|
|
1504
|
+
case "drawText":
|
|
1505
|
+
break;
|
|
1506
|
+
// 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
|
|
1507
|
+
// 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
|
|
1508
|
+
// 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
|
|
1509
|
+
case "equation": {
|
|
1510
|
+
const script = findChildByLocalName(child, "script");
|
|
1511
|
+
const raw = script ? extractTextFromNode(script) : "";
|
|
1512
|
+
if (raw.trim()) {
|
|
1513
|
+
try {
|
|
1514
|
+
const latex = hmlToLatex(raw).trim();
|
|
1515
|
+
if (latex) text += " $" + latex + "$ ";
|
|
1516
|
+
} catch {
|
|
1517
|
+
}
|
|
1423
1518
|
}
|
|
1519
|
+
break;
|
|
1424
1520
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
case "drawingObject": {
|
|
1432
|
-
if (tableCtx?.cell) {
|
|
1433
|
-
const sink = [];
|
|
1434
|
-
handleShape(el, sink, ctx);
|
|
1435
|
-
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1436
|
-
} else {
|
|
1437
|
-
handleShape(el, blocks, ctx);
|
|
1438
|
-
}
|
|
1439
|
-
break;
|
|
1440
|
-
}
|
|
1441
|
-
// 메모 — 본문 혼입 차단 (v3.0)
|
|
1442
|
-
case "memogroup":
|
|
1443
|
-
case "memo": {
|
|
1444
|
-
if (ctx.warnings && extractTextFromNode(el)) {
|
|
1445
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
|
|
1521
|
+
// run 요소에서 charPrIDRef 추출
|
|
1522
|
+
case "r": {
|
|
1523
|
+
const runCharPr = child.getAttribute("charPrIDRef");
|
|
1524
|
+
if (runCharPr && !charPrId) charPrId = runCharPr;
|
|
1525
|
+
walk(child);
|
|
1526
|
+
break;
|
|
1446
1527
|
}
|
|
1447
|
-
|
|
1528
|
+
default:
|
|
1529
|
+
walk(child);
|
|
1530
|
+
break;
|
|
1448
1531
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
walk(para);
|
|
1535
|
+
const leaderIdx = text.indexOf("");
|
|
1536
|
+
if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
|
|
1537
|
+
let cleanText = text.replace(/[ \t]+/g, " ").trim();
|
|
1538
|
+
if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
|
|
1539
|
+
cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
|
|
1540
|
+
cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
|
|
1541
|
+
let style;
|
|
1542
|
+
if (styleMap && charPrId) {
|
|
1543
|
+
const charProp = styleMap.charProperties.get(charPrId);
|
|
1544
|
+
if (charProp) {
|
|
1545
|
+
style = {};
|
|
1546
|
+
if (charProp.fontSize) style.fontSize = charProp.fontSize;
|
|
1547
|
+
if (charProp.bold) style.bold = true;
|
|
1548
|
+
if (charProp.italic) style.italic = true;
|
|
1549
|
+
if (charProp.fontName) style.fontName = charProp.fontName;
|
|
1550
|
+
if (!style.fontSize && !style.bold && !style.italic) style = void 0;
|
|
1452
1551
|
}
|
|
1453
1552
|
}
|
|
1553
|
+
return { text: cleanText, href, footnote, style };
|
|
1454
1554
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1555
|
+
|
|
1556
|
+
// src/hwpx/images.ts
|
|
1557
|
+
function imageExtToMime(ext) {
|
|
1558
|
+
switch (ext.toLowerCase()) {
|
|
1559
|
+
case "jpg":
|
|
1560
|
+
case "jpeg":
|
|
1561
|
+
return "image/jpeg";
|
|
1562
|
+
case "png":
|
|
1563
|
+
return "image/png";
|
|
1564
|
+
case "gif":
|
|
1565
|
+
return "image/gif";
|
|
1566
|
+
case "bmp":
|
|
1567
|
+
return "image/bmp";
|
|
1568
|
+
case "tif":
|
|
1569
|
+
case "tiff":
|
|
1570
|
+
return "image/tiff";
|
|
1571
|
+
case "wmf":
|
|
1572
|
+
return "image/wmf";
|
|
1573
|
+
case "emf":
|
|
1574
|
+
return "image/emf";
|
|
1575
|
+
case "svg":
|
|
1576
|
+
return "image/svg+xml";
|
|
1577
|
+
default:
|
|
1578
|
+
return "application/octet-stream";
|
|
1475
1579
|
}
|
|
1476
1580
|
}
|
|
1477
|
-
function
|
|
1478
|
-
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
if (
|
|
1482
|
-
if (
|
|
1483
|
-
if (
|
|
1484
|
-
return
|
|
1581
|
+
function mimeToExt(mime) {
|
|
1582
|
+
if (mime.includes("jpeg")) return "jpg";
|
|
1583
|
+
if (mime.includes("png")) return "png";
|
|
1584
|
+
if (mime.includes("gif")) return "gif";
|
|
1585
|
+
if (mime.includes("bmp")) return "bmp";
|
|
1586
|
+
if (mime.includes("tiff")) return "tif";
|
|
1587
|
+
if (mime.includes("wmf")) return "wmf";
|
|
1588
|
+
if (mime.includes("emf")) return "emf";
|
|
1589
|
+
if (mime.includes("svg")) return "svg";
|
|
1590
|
+
return "bin";
|
|
1485
1591
|
}
|
|
1486
|
-
function
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
(
|
|
1491
|
-
} else if (
|
|
1492
|
-
|
|
1493
|
-
|
|
1592
|
+
function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
|
|
1593
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
1594
|
+
for (const block of blocks) {
|
|
1595
|
+
if (block.type === "image") {
|
|
1596
|
+
out.push({ block, ownerCell });
|
|
1597
|
+
} else if (block.type === "table" && block.table) {
|
|
1598
|
+
for (const row of block.table.cells) {
|
|
1599
|
+
for (const cell of row) {
|
|
1600
|
+
if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
|
|
1601
|
+
}
|
|
1494
1602
|
}
|
|
1495
|
-
;
|
|
1496
|
-
(cell.blocks ??= []).push(b);
|
|
1497
|
-
cell.hasStructure = true;
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
function collectSubListText(el, ctx, depth = 0) {
|
|
1502
|
-
if (depth > 10) return "";
|
|
1503
|
-
const parts = [];
|
|
1504
|
-
const children = el.childNodes;
|
|
1505
|
-
if (!children) return "";
|
|
1506
|
-
for (let i = 0; i < children.length; i++) {
|
|
1507
|
-
const ch = children[i];
|
|
1508
|
-
if (ch.nodeType !== 1) continue;
|
|
1509
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1510
|
-
if (tag === "p" || tag === "para") {
|
|
1511
|
-
const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
|
|
1512
|
-
if (t) parts.push(t);
|
|
1513
|
-
} else if (tag === "tbl") {
|
|
1514
|
-
continue;
|
|
1515
|
-
} else {
|
|
1516
|
-
const t = collectSubListText(ch, ctx, depth + 1);
|
|
1517
|
-
if (t) parts.push(t);
|
|
1518
1603
|
}
|
|
1519
1604
|
}
|
|
1520
|
-
return parts.join("\n").trim();
|
|
1521
1605
|
}
|
|
1522
|
-
function
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
if (!
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1606
|
+
async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
|
|
1607
|
+
const images = [];
|
|
1608
|
+
let imageIndex = 0;
|
|
1609
|
+
const imageBlocks = [];
|
|
1610
|
+
collectImageBlocks(blocks, imageBlocks);
|
|
1611
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1612
|
+
for (const { block, ownerCell } of imageBlocks) {
|
|
1613
|
+
if (block.type !== "image" || !block.text) continue;
|
|
1614
|
+
const ref = block.text;
|
|
1615
|
+
let img = resolved.get(ref);
|
|
1616
|
+
if (img === void 0) {
|
|
1617
|
+
img = null;
|
|
1618
|
+
const candidates = [
|
|
1619
|
+
`BinData/${ref}`,
|
|
1620
|
+
`Contents/BinData/${ref}`,
|
|
1621
|
+
ref
|
|
1622
|
+
// 절대 경로일 수도 있음
|
|
1623
|
+
];
|
|
1624
|
+
let resolvedPath = null;
|
|
1625
|
+
if (!ref.includes(".")) {
|
|
1626
|
+
const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
|
|
1627
|
+
for (const prefix of prefixes) {
|
|
1628
|
+
const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
|
|
1629
|
+
if (match.length > 0) {
|
|
1630
|
+
resolvedPath = match[0].name;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1547
1633
|
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1634
|
+
}
|
|
1635
|
+
const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
|
|
1636
|
+
for (const path of allCandidates) {
|
|
1637
|
+
if (isPathTraversal(path)) continue;
|
|
1638
|
+
const file = zip.file(path);
|
|
1639
|
+
if (!file) continue;
|
|
1640
|
+
try {
|
|
1641
|
+
const data = await file.async("uint8array");
|
|
1642
|
+
decompressed.total += data.length;
|
|
1643
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1644
|
+
const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
|
|
1645
|
+
const mimeType = imageExtToMime(ext);
|
|
1646
|
+
imageIndex++;
|
|
1647
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
1648
|
+
img = { filename, data, mimeType };
|
|
1649
|
+
images.push(img);
|
|
1650
|
+
break;
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
if (err instanceof KordocError) throw err;
|
|
1555
1653
|
}
|
|
1556
|
-
} else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
|
|
1557
|
-
walkChildren(el, d + 1);
|
|
1558
1654
|
}
|
|
1655
|
+
if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
|
|
1656
|
+
resolved.set(ref, img);
|
|
1559
1657
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const child = children[i];
|
|
1570
|
-
if (child.nodeType !== 1) continue;
|
|
1571
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1572
|
-
if (tag === targetTag) return child;
|
|
1573
|
-
const found = findDescendant(child, targetTag, depth + 1);
|
|
1574
|
-
if (found) return found;
|
|
1658
|
+
if (!img) {
|
|
1659
|
+
block.type = "paragraph";
|
|
1660
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
1661
|
+
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, `[\uC774\uBBF8\uC9C0: ${ref}]`);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
block.text = img.filename;
|
|
1665
|
+
block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
|
|
1666
|
+
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, ``);
|
|
1575
1667
|
}
|
|
1576
|
-
return
|
|
1668
|
+
return images;
|
|
1577
1669
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1670
|
+
|
|
1671
|
+
// src/hwpx/metadata.ts
|
|
1672
|
+
import JSZip from "jszip";
|
|
1673
|
+
|
|
1674
|
+
// src/hwpx/zip-sections.ts
|
|
1675
|
+
import { inflateRawSync } from "zlib";
|
|
1676
|
+
function extractFromBrokenZip(buffer) {
|
|
1677
|
+
const data = new Uint8Array(buffer);
|
|
1678
|
+
const view = new DataView(buffer);
|
|
1679
|
+
let pos = 0;
|
|
1680
|
+
const blocks = [];
|
|
1681
|
+
const warnings = [
|
|
1682
|
+
{ code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
|
|
1683
|
+
];
|
|
1684
|
+
let totalDecompressed = 0;
|
|
1685
|
+
let entryCount = 0;
|
|
1686
|
+
let sectionNum = 0;
|
|
1687
|
+
const shared = createSectionShared();
|
|
1688
|
+
while (pos < data.length - 30) {
|
|
1689
|
+
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
1690
|
+
pos++;
|
|
1691
|
+
while (pos < data.length - 30) {
|
|
1692
|
+
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
1693
|
+
pos++;
|
|
1600
1694
|
}
|
|
1695
|
+
continue;
|
|
1601
1696
|
}
|
|
1602
|
-
|
|
1697
|
+
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
1698
|
+
const method = view.getUint16(pos + 8, true);
|
|
1699
|
+
const compSize = view.getUint32(pos + 18, true);
|
|
1700
|
+
const nameLen = view.getUint16(pos + 26, true);
|
|
1701
|
+
const extraLen = view.getUint16(pos + 28, true);
|
|
1702
|
+
if (nameLen > 1024 || extraLen > 65535) {
|
|
1703
|
+
pos += 30 + nameLen + extraLen;
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
const fileStart = pos + 30 + nameLen + extraLen;
|
|
1707
|
+
if (fileStart + compSize > data.length) break;
|
|
1708
|
+
if (compSize === 0 && method !== 0) {
|
|
1709
|
+
pos = fileStart;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
|
|
1713
|
+
const name = new TextDecoder().decode(nameBytes);
|
|
1714
|
+
if (isPathTraversal(name)) {
|
|
1715
|
+
pos = fileStart + compSize;
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
const fileData = data.slice(fileStart, fileStart + compSize);
|
|
1719
|
+
pos = fileStart + compSize;
|
|
1720
|
+
if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
|
|
1721
|
+
try {
|
|
1722
|
+
let content;
|
|
1723
|
+
if (method === 0) {
|
|
1724
|
+
content = new TextDecoder().decode(fileData);
|
|
1725
|
+
} else if (method === 8) {
|
|
1726
|
+
const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
|
|
1727
|
+
content = new TextDecoder().decode(decompressed);
|
|
1728
|
+
} else {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
totalDecompressed += content.length * 2;
|
|
1732
|
+
if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
|
|
1733
|
+
sectionNum++;
|
|
1734
|
+
blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
|
|
1735
|
+
} catch {
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (blocks.length === 0) throw new KordocError("\uC190\uC0C1\uB41C HWPX\uC5D0\uC11C \uC139\uC158 \uB370\uC774\uD130\uB97C \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1740
|
+
applyPageText(blocks, shared);
|
|
1741
|
+
const markdown = blocksToMarkdown(blocks);
|
|
1742
|
+
return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1603
1743
|
}
|
|
1604
|
-
function
|
|
1605
|
-
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
if (
|
|
1613
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1614
|
-
if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
|
|
1615
|
-
let url = (ch.textContent || "").trim();
|
|
1616
|
-
if (!url) continue;
|
|
1617
|
-
url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
|
|
1618
|
-
const safe = sanitizeHref(url);
|
|
1619
|
-
if (safe) return safe;
|
|
1744
|
+
async function resolveSectionPaths(zip) {
|
|
1745
|
+
const manifestPaths = ["Contents/content.hpf", "content.hpf"];
|
|
1746
|
+
for (const mp of manifestPaths) {
|
|
1747
|
+
const mpLower = mp.toLowerCase();
|
|
1748
|
+
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
|
|
1749
|
+
if (!file) continue;
|
|
1750
|
+
const xml = await file.async("text");
|
|
1751
|
+
const paths = parseSectionPathsFromManifest(xml);
|
|
1752
|
+
if (paths.length > 0) return paths;
|
|
1620
1753
|
}
|
|
1621
|
-
|
|
1754
|
+
const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
|
|
1755
|
+
return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
|
|
1622
1756
|
}
|
|
1623
|
-
function
|
|
1624
|
-
|
|
1757
|
+
function parseSectionPathsFromManifest(xml) {
|
|
1758
|
+
const parser = createXmlParser();
|
|
1759
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1760
|
+
const items = doc.getElementsByTagName("opf:item");
|
|
1761
|
+
const spine = doc.getElementsByTagName("opf:itemref");
|
|
1762
|
+
const idToHref = /* @__PURE__ */ new Map();
|
|
1763
|
+
for (let i = 0; i < items.length; i++) {
|
|
1764
|
+
const item = items[i];
|
|
1765
|
+
const id = item.getAttribute("id") || "";
|
|
1766
|
+
const href = normalizeSectionHref(item.getAttribute("href") || "");
|
|
1767
|
+
if (id && href) idToHref.set(id, href);
|
|
1768
|
+
}
|
|
1769
|
+
if (spine.length > 0) {
|
|
1770
|
+
const ordered = [];
|
|
1771
|
+
for (let i = 0; i < spine.length; i++) {
|
|
1772
|
+
const href = idToHref.get(spine[i].getAttribute("idref") || "");
|
|
1773
|
+
if (href) ordered.push(href);
|
|
1774
|
+
}
|
|
1775
|
+
if (ordered.length > 0) return ordered;
|
|
1776
|
+
}
|
|
1777
|
+
return Array.from(idToHref.values()).sort(compareSectionPaths);
|
|
1625
1778
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
switch (ktag) {
|
|
1639
|
-
// 머리말/꼬리말 — 문서당 1회 수집, 본문 앞/뒤 배치
|
|
1640
|
-
case "header":
|
|
1641
|
-
case "footer": {
|
|
1642
|
-
if (!ctx) break;
|
|
1643
|
-
const t = collectSubListText(k, ctx);
|
|
1644
|
-
if (t) {
|
|
1645
|
-
const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
|
|
1646
|
-
if (!bucket.includes(t)) bucket.push(t);
|
|
1647
|
-
}
|
|
1648
|
-
break;
|
|
1649
|
-
}
|
|
1650
|
-
// 각주/미주 — 해당 문단의 footnote로 인라인 보존
|
|
1651
|
-
case "footNote":
|
|
1652
|
-
case "endNote": {
|
|
1653
|
-
const noteText = extractTextFromNode(k);
|
|
1654
|
-
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1655
|
-
break;
|
|
1656
|
-
}
|
|
1657
|
-
// 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
|
|
1658
|
-
case "fieldBegin": {
|
|
1659
|
-
const url = extractHyperlinkHref(k);
|
|
1660
|
-
if (url && !href) href = url;
|
|
1661
|
-
break;
|
|
1662
|
-
}
|
|
1663
|
-
case "fieldEnd":
|
|
1664
|
-
break;
|
|
1665
|
-
// 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
|
|
1666
|
-
case "deleteBegin":
|
|
1667
|
-
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1668
|
-
break;
|
|
1669
|
-
case "deleteEnd":
|
|
1670
|
-
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1671
|
-
break;
|
|
1672
|
-
case "insertBegin":
|
|
1673
|
-
case "insertEnd":
|
|
1674
|
-
break;
|
|
1675
|
-
// 삽입분은 최종본에 포함
|
|
1676
|
-
// 숨은 설명 — 본문 혼입 차단
|
|
1677
|
-
case "hiddenComment": {
|
|
1678
|
-
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1679
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
|
|
1680
|
-
}
|
|
1681
|
-
break;
|
|
1682
|
-
}
|
|
1683
|
-
// 콘텐츠 없는 제어 요소 — 스킵
|
|
1684
|
-
case "bookmark":
|
|
1685
|
-
case "pageNum":
|
|
1686
|
-
case "pageNumCtrl":
|
|
1687
|
-
case "pageHiding":
|
|
1688
|
-
case "newNum":
|
|
1689
|
-
case "autoNum":
|
|
1690
|
-
case "indexmark":
|
|
1691
|
-
case "colPr":
|
|
1692
|
-
break;
|
|
1693
|
-
// 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
|
|
1694
|
-
default: {
|
|
1695
|
-
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1696
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1779
|
+
|
|
1780
|
+
// src/hwpx/metadata.ts
|
|
1781
|
+
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
1782
|
+
try {
|
|
1783
|
+
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
1784
|
+
for (const mp of metaPaths) {
|
|
1785
|
+
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
1786
|
+
if (!file) continue;
|
|
1787
|
+
const xml = await file.async("text");
|
|
1788
|
+
if (decompressed) {
|
|
1789
|
+
decompressed.total += xml.length * 2;
|
|
1790
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1699
1791
|
}
|
|
1792
|
+
parseDublinCoreMetadata(xml, metadata);
|
|
1793
|
+
if (metadata.title || metadata.author) return;
|
|
1700
1794
|
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
} else {
|
|
1715
|
-
text += t;
|
|
1716
|
-
}
|
|
1717
|
-
continue;
|
|
1718
|
-
}
|
|
1719
|
-
if (child.nodeType !== 1) continue;
|
|
1720
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1721
|
-
switch (tag) {
|
|
1722
|
-
case "t":
|
|
1723
|
-
walk(child);
|
|
1724
|
-
break;
|
|
1725
|
-
// 자식 순회 (tab 등 하위 요소 처리)
|
|
1726
|
-
case "tab": {
|
|
1727
|
-
const leader = child.getAttribute("leader");
|
|
1728
|
-
if (leader && leader !== "0") {
|
|
1729
|
-
text += "";
|
|
1730
|
-
} else {
|
|
1731
|
-
text += " ";
|
|
1732
|
-
}
|
|
1733
|
-
break;
|
|
1734
|
-
}
|
|
1735
|
-
case "br":
|
|
1736
|
-
if ((child.getAttribute("type") || "line") === "line") text += "\n";
|
|
1737
|
-
break;
|
|
1738
|
-
case "lineBreak":
|
|
1739
|
-
text += "\n";
|
|
1740
|
-
break;
|
|
1741
|
-
// 강제 줄바꿈 — ref 추출기·소스맵 스캐너와 동일 모델
|
|
1742
|
-
case "fwSpace":
|
|
1743
|
-
case "hwSpace":
|
|
1744
|
-
text += " ";
|
|
1745
|
-
break;
|
|
1746
|
-
case "tbl":
|
|
1747
|
-
break;
|
|
1748
|
-
// 테이블은 walkSection에서 처리
|
|
1749
|
-
// 하이퍼링크
|
|
1750
|
-
case "hyperlink": {
|
|
1751
|
-
const url = child.getAttribute("url") || child.getAttribute("href") || "";
|
|
1752
|
-
if (url) {
|
|
1753
|
-
const safe = sanitizeHref(url);
|
|
1754
|
-
if (safe) href = safe;
|
|
1755
|
-
}
|
|
1756
|
-
walk(child);
|
|
1757
|
-
break;
|
|
1758
|
-
}
|
|
1759
|
-
// 각주/미주
|
|
1760
|
-
case "footNote":
|
|
1761
|
-
case "endNote":
|
|
1762
|
-
case "fn":
|
|
1763
|
-
case "en": {
|
|
1764
|
-
const noteText = extractTextFromNode(child);
|
|
1765
|
-
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1766
|
-
break;
|
|
1767
|
-
}
|
|
1768
|
-
// 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
|
|
1769
|
-
case "ctrl":
|
|
1770
|
-
handleCtrl(child);
|
|
1771
|
-
break;
|
|
1772
|
-
// run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
|
|
1773
|
-
case "fieldBegin": {
|
|
1774
|
-
const url = extractHyperlinkHref(child);
|
|
1775
|
-
if (url && !href) href = url;
|
|
1776
|
-
break;
|
|
1777
|
-
}
|
|
1778
|
-
// run 직계 변경추적 마커 (비표준 경로)
|
|
1779
|
-
case "deleteBegin":
|
|
1780
|
-
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1781
|
-
break;
|
|
1782
|
-
case "deleteEnd":
|
|
1783
|
-
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1784
|
-
break;
|
|
1785
|
-
case "insertBegin":
|
|
1786
|
-
case "insertEnd":
|
|
1787
|
-
break;
|
|
1788
|
-
case "fieldEnd":
|
|
1789
|
-
case "parameters":
|
|
1790
|
-
case "stringParam":
|
|
1791
|
-
case "integerParam":
|
|
1792
|
-
case "boolParam":
|
|
1793
|
-
case "floatParam":
|
|
1794
|
-
case "secPr":
|
|
1795
|
-
// 섹션 속성 (페이지 설정 등)
|
|
1796
|
-
case "colPr":
|
|
1797
|
-
// 다단 속성
|
|
1798
|
-
case "linesegarray":
|
|
1799
|
-
case "lineseg":
|
|
1800
|
-
// 레이아웃 정보
|
|
1801
|
-
// 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
|
|
1802
|
-
case "pic":
|
|
1803
|
-
case "shape":
|
|
1804
|
-
case "drawingObject":
|
|
1805
|
-
case "shapeComment":
|
|
1806
|
-
case "drawText":
|
|
1807
|
-
break;
|
|
1808
|
-
// 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
|
|
1809
|
-
// 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
|
|
1810
|
-
// 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
|
|
1811
|
-
case "equation": {
|
|
1812
|
-
const script = findChildByLocalName(child, "script");
|
|
1813
|
-
const raw = script ? extractTextFromNode(script) : "";
|
|
1814
|
-
if (raw.trim()) {
|
|
1815
|
-
try {
|
|
1816
|
-
const latex = hmlToLatex(raw).trim();
|
|
1817
|
-
if (latex) text += " $" + latex + "$ ";
|
|
1818
|
-
} catch {
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
break;
|
|
1822
|
-
}
|
|
1823
|
-
// run 요소에서 charPrIDRef 추출
|
|
1824
|
-
case "r": {
|
|
1825
|
-
const runCharPr = child.getAttribute("charPrIDRef");
|
|
1826
|
-
if (runCharPr && !charPrId) charPrId = runCharPr;
|
|
1827
|
-
walk(child);
|
|
1828
|
-
break;
|
|
1829
|
-
}
|
|
1830
|
-
default:
|
|
1831
|
-
walk(child);
|
|
1832
|
-
break;
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function parseDublinCoreMetadata(xml, metadata) {
|
|
1799
|
+
const parser = createXmlParser();
|
|
1800
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1801
|
+
if (!doc.documentElement) return;
|
|
1802
|
+
const getText = (tagNames) => {
|
|
1803
|
+
for (const tag of tagNames) {
|
|
1804
|
+
const els = doc.getElementsByTagName(tag);
|
|
1805
|
+
if (els.length > 0) {
|
|
1806
|
+
const text = els[0].textContent?.trim();
|
|
1807
|
+
if (text) return text;
|
|
1833
1808
|
}
|
|
1834
1809
|
}
|
|
1810
|
+
return void 0;
|
|
1835
1811
|
};
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
if (styleMap && charPrId) {
|
|
1845
|
-
const charProp = styleMap.charProperties.get(charPrId);
|
|
1846
|
-
if (charProp) {
|
|
1847
|
-
style = {};
|
|
1848
|
-
if (charProp.fontSize) style.fontSize = charProp.fontSize;
|
|
1849
|
-
if (charProp.bold) style.bold = true;
|
|
1850
|
-
if (charProp.italic) style.italic = true;
|
|
1851
|
-
if (charProp.fontName) style.fontName = charProp.fontName;
|
|
1852
|
-
if (!style.fontSize && !style.bold && !style.italic) style = void 0;
|
|
1853
|
-
}
|
|
1812
|
+
metadata.title = metadata.title || getText(["dc:title", "title"]);
|
|
1813
|
+
metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
|
|
1814
|
+
metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
|
|
1815
|
+
metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
|
|
1816
|
+
metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
|
|
1817
|
+
const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
|
|
1818
|
+
if (keywords && !metadata.keywords) {
|
|
1819
|
+
metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
1854
1820
|
}
|
|
1855
|
-
return { text: cleanText, href, footnote, style };
|
|
1856
1821
|
}
|
|
1857
|
-
function
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1864
|
-
if (tag === name) return ch;
|
|
1822
|
+
async function extractHwpxMetadataOnly(buffer) {
|
|
1823
|
+
let zip;
|
|
1824
|
+
try {
|
|
1825
|
+
zip = await JSZip.loadAsync(buffer);
|
|
1826
|
+
} catch {
|
|
1827
|
+
throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1865
1828
|
}
|
|
1866
|
-
|
|
1829
|
+
const metadata = {};
|
|
1830
|
+
await extractHwpxMetadata(zip, metadata);
|
|
1831
|
+
const sectionPaths = await resolveSectionPaths(zip);
|
|
1832
|
+
metadata.pageCount = sectionPaths.length;
|
|
1833
|
+
return metadata;
|
|
1867
1834
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1835
|
+
|
|
1836
|
+
// src/hwpx/parser.ts
|
|
1837
|
+
async function parseHwpxDocument(buffer, options) {
|
|
1838
|
+
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
|
|
1839
|
+
let zip;
|
|
1840
|
+
try {
|
|
1841
|
+
zip = await JSZip2.loadAsync(buffer);
|
|
1842
|
+
} catch {
|
|
1843
|
+
return extractFromBrokenZip(buffer);
|
|
1876
1844
|
}
|
|
1877
|
-
|
|
1845
|
+
const actualEntryCount = Object.keys(zip.files).length;
|
|
1846
|
+
if (actualEntryCount > MAX_ZIP_ENTRIES) {
|
|
1847
|
+
throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1848
|
+
}
|
|
1849
|
+
const manifestFile = zip.file("META-INF/manifest.xml");
|
|
1850
|
+
if (manifestFile) {
|
|
1851
|
+
const manifestXml = await manifestFile.async("text");
|
|
1852
|
+
if (isEncryptedHwpx(manifestXml)) {
|
|
1853
|
+
if (isComFallbackAvailable() && options?.filePath) {
|
|
1854
|
+
const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
|
|
1855
|
+
if (pages.some((p) => p && p.trim().length > 0)) {
|
|
1856
|
+
return comResultToParseResult(pages, pageCount, warnings2);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const decompressed = { total: 0 };
|
|
1863
|
+
const metadata = {};
|
|
1864
|
+
await extractHwpxMetadata(zip, metadata, decompressed);
|
|
1865
|
+
const styleMap = await extractHwpxStyles(zip, decompressed);
|
|
1866
|
+
const warnings = [];
|
|
1867
|
+
const sectionPaths = await resolveSectionPaths(zip);
|
|
1868
|
+
if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1869
|
+
metadata.pageCount = sectionPaths.length;
|
|
1870
|
+
const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
|
|
1871
|
+
const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
|
|
1872
|
+
const blocks = [];
|
|
1873
|
+
const shared = createSectionShared();
|
|
1874
|
+
let parsedSections = 0;
|
|
1875
|
+
for (let si = 0; si < sectionPaths.length; si++) {
|
|
1876
|
+
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
1877
|
+
const file = zip.file(sectionPaths[si]);
|
|
1878
|
+
if (!file) continue;
|
|
1879
|
+
try {
|
|
1880
|
+
const xml = await file.async("text");
|
|
1881
|
+
decompressed.total += xml.length * 2;
|
|
1882
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1883
|
+
blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
|
|
1884
|
+
parsedSections++;
|
|
1885
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
1886
|
+
} catch (secErr) {
|
|
1887
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
1888
|
+
warnings.push({ page: si + 1, message: `\uC139\uC158 ${si + 1} \uD30C\uC2F1 \uC2E4\uD328: ${secErr instanceof Error ? secErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
applyPageText(blocks, shared);
|
|
1892
|
+
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
1893
|
+
detectHwpxHeadings(blocks, styleMap);
|
|
1894
|
+
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
1895
|
+
const markdown = blocksToMarkdown(blocks);
|
|
1896
|
+
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
|
|
1878
1897
|
}
|
|
1879
1898
|
|
|
1880
1899
|
// src/hwp5/record.ts
|
|
@@ -16530,7 +16549,7 @@ function isDistributionSentinel(markdown) {
|
|
|
16530
16549
|
}
|
|
16531
16550
|
|
|
16532
16551
|
// src/xlsx/parser.ts
|
|
16533
|
-
import
|
|
16552
|
+
import JSZip3 from "jszip";
|
|
16534
16553
|
import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
|
|
16535
16554
|
var MAX_SHEETS = 100;
|
|
16536
16555
|
var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
|
|
@@ -16721,7 +16740,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
|
|
|
16721
16740
|
}
|
|
16722
16741
|
async function parseXlsxDocument(buffer, options) {
|
|
16723
16742
|
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
|
|
16724
|
-
const zip = await
|
|
16743
|
+
const zip = await JSZip3.loadAsync(buffer);
|
|
16725
16744
|
const warnings = [];
|
|
16726
16745
|
const workbookFile = zip.file("xl/workbook.xml");
|
|
16727
16746
|
if (!workbookFile) {
|
|
@@ -17427,7 +17446,7 @@ async function parseXlsDocument(buffer, options) {
|
|
|
17427
17446
|
}
|
|
17428
17447
|
|
|
17429
17448
|
// src/docx/parser.ts
|
|
17430
|
-
import
|
|
17449
|
+
import JSZip4 from "jszip";
|
|
17431
17450
|
import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
|
|
17432
17451
|
|
|
17433
17452
|
// src/docx/equation.ts
|
|
@@ -18181,7 +18200,7 @@ async function extractImages(zip, rels, doc, warnings) {
|
|
|
18181
18200
|
}
|
|
18182
18201
|
async function parseDocxDocument(buffer, options) {
|
|
18183
18202
|
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
|
|
18184
|
-
const zip = await
|
|
18203
|
+
const zip = await JSZip4.loadAsync(buffer);
|
|
18185
18204
|
const warnings = [];
|
|
18186
18205
|
const docFile = zip.file("word/document.xml");
|
|
18187
18206
|
if (!docFile) {
|
|
@@ -19099,7 +19118,7 @@ function fillInlineFields(text, values, filled, matchedLabels) {
|
|
|
19099
19118
|
}
|
|
19100
19119
|
|
|
19101
19120
|
// src/form/filler-hwpx.ts
|
|
19102
|
-
import
|
|
19121
|
+
import JSZip5 from "jszip";
|
|
19103
19122
|
|
|
19104
19123
|
// src/roundtrip/source-map.ts
|
|
19105
19124
|
function escapeXmlText(text) {
|
|
@@ -19773,7 +19792,7 @@ function patchZipEntries(original, replacements) {
|
|
|
19773
19792
|
// src/form/filler-hwpx.ts
|
|
19774
19793
|
async function fillHwpx(hwpxBuffer, values) {
|
|
19775
19794
|
const u8 = new Uint8Array(hwpxBuffer);
|
|
19776
|
-
const zip = await
|
|
19795
|
+
const zip = await JSZip5.loadAsync(hwpxBuffer);
|
|
19777
19796
|
const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
|
|
19778
19797
|
if (sectionPaths.length === 0) {
|
|
19779
19798
|
throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
@@ -20014,7 +20033,7 @@ async function fillHwpx(hwpxBuffer, values) {
|
|
|
20014
20033
|
}
|
|
20015
20034
|
|
|
20016
20035
|
// src/hwpx/generator.ts
|
|
20017
|
-
import
|
|
20036
|
+
import JSZip6 from "jszip";
|
|
20018
20037
|
|
|
20019
20038
|
// src/hwpx/text-metrics.ts
|
|
20020
20039
|
var ASCII_W = [
|
|
@@ -20385,53 +20404,112 @@ function mmToHwpunit(mm) {
|
|
|
20385
20404
|
return Math.round(mm * 7200 / 25.4);
|
|
20386
20405
|
}
|
|
20387
20406
|
|
|
20388
|
-
// src/
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20407
|
+
// src/hwpx/gen-ids.ts
|
|
20408
|
+
var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
|
|
20409
|
+
var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
|
|
20410
|
+
var NS_HEAD = "http://www.hancom.co.kr/hwpml/2011/head";
|
|
20411
|
+
var NS_CORE = "http://www.hancom.co.kr/hwpml/2011/core";
|
|
20412
|
+
var NS_OPF = "http://www.idpf.org/2007/opf/";
|
|
20413
|
+
var NS_HPF = "http://www.hancom.co.kr/schema/2011/hpf";
|
|
20414
|
+
var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
|
|
20415
|
+
var CHAR_NORMAL = 0;
|
|
20416
|
+
var CHAR_BOLD = 1;
|
|
20417
|
+
var CHAR_ITALIC = 2;
|
|
20418
|
+
var CHAR_BOLD_ITALIC = 3;
|
|
20419
|
+
var CHAR_CODE = 4;
|
|
20420
|
+
var CHAR_H1 = 5;
|
|
20421
|
+
var CHAR_H2 = 6;
|
|
20422
|
+
var CHAR_H3 = 7;
|
|
20423
|
+
var CHAR_H4 = 8;
|
|
20424
|
+
var CHAR_TABLE_HEADER = 9;
|
|
20425
|
+
var CHAR_QUOTE = 10;
|
|
20426
|
+
var PARA_NORMAL = 0;
|
|
20427
|
+
var PARA_H1 = 1;
|
|
20428
|
+
var PARA_H2 = 2;
|
|
20429
|
+
var PARA_H3 = 3;
|
|
20430
|
+
var PARA_H4 = 4;
|
|
20431
|
+
var PARA_CODE = 5;
|
|
20432
|
+
var PARA_QUOTE = 6;
|
|
20433
|
+
var PARA_LIST = 7;
|
|
20434
|
+
var DEFAULT_TEXT_COLOR = "#000000";
|
|
20435
|
+
function resolveTheme(theme) {
|
|
20436
|
+
return {
|
|
20437
|
+
h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
|
|
20438
|
+
h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
|
|
20439
|
+
h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20440
|
+
h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20441
|
+
body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20442
|
+
quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
|
|
20443
|
+
/** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
|
|
20444
|
+
hasQuoteOption: theme?.quoteColor !== void 0,
|
|
20445
|
+
tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20446
|
+
tableHeaderBold: !!theme?.tableHeaderBold
|
|
20447
|
+
};
|
|
20395
20448
|
}
|
|
20396
|
-
function
|
|
20397
|
-
return
|
|
20449
|
+
function escapeXml(text) {
|
|
20450
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20398
20451
|
}
|
|
20399
|
-
function
|
|
20400
|
-
|
|
20452
|
+
function headingParaPrId(level) {
|
|
20453
|
+
if (level === 1) return PARA_H1;
|
|
20454
|
+
if (level === 2) return PARA_H2;
|
|
20455
|
+
if (level === 3) return PARA_H3;
|
|
20456
|
+
return PARA_H4;
|
|
20401
20457
|
}
|
|
20402
|
-
|
|
20403
|
-
|
|
20404
|
-
if (
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
|
|
20408
|
-
|
|
20409
|
-
|
|
20410
|
-
|
|
20411
|
-
|
|
20412
|
-
|
|
20413
|
-
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20458
|
+
function headingCharPrId(level) {
|
|
20459
|
+
if (level === 1) return CHAR_H1;
|
|
20460
|
+
if (level === 2) return CHAR_H2;
|
|
20461
|
+
if (level === 3) return CHAR_H3;
|
|
20462
|
+
return CHAR_H4;
|
|
20463
|
+
}
|
|
20464
|
+
function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_COLOR, ratioPct = 100) {
|
|
20465
|
+
const boldAttr = bold ? ` bold="1"` : "";
|
|
20466
|
+
const italicAttr = italic ? ` italic="1"` : "";
|
|
20467
|
+
const effFont = bold ? 2 : fontId;
|
|
20468
|
+
return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
|
|
20469
|
+
<hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
|
|
20470
|
+
<hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
|
|
20471
|
+
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
20472
|
+
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
20473
|
+
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
20474
|
+
</hh:charPr>`;
|
|
20475
|
+
}
|
|
20476
|
+
function paraPr(id, opts = {}) {
|
|
20477
|
+
const { align = "JUSTIFY", spaceBefore = 0, spaceAfter = 0, lineSpacing = 160, indent = 0, left = 0, keepWord = false } = opts;
|
|
20478
|
+
const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
|
|
20479
|
+
const snapGrid = keepWord ? "0" : "1";
|
|
20480
|
+
return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
|
|
20481
|
+
<hh:align horizontal="${align}" vertical="BASELINE"/>
|
|
20482
|
+
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
20483
|
+
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
20484
|
+
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
20485
|
+
<hh:margin><hc:intent value="${indent}" unit="HWPUNIT"/><hc:left value="${left}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="${spaceBefore}" unit="HWPUNIT"/><hc:next value="${spaceAfter}" unit="HWPUNIT"/></hh:margin>
|
|
20486
|
+
<hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
|
|
20487
|
+
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
20488
|
+
</hh:paraPr>`;
|
|
20489
|
+
}
|
|
20490
|
+
var GONGMUN_LIST_BASE = 8;
|
|
20491
|
+
var GONGMUN_LIST_LEVELS = 8;
|
|
20492
|
+
var GONGMUN_CENTER = GONGMUN_LIST_BASE + GONGMUN_LIST_LEVELS;
|
|
20493
|
+
var CHAR_VARIANT_BASE = 11;
|
|
20494
|
+
var GONGMUN_BODY_RATIO = 95;
|
|
20495
|
+
|
|
20496
|
+
// src/hwpx/md-runs.ts
|
|
20497
|
+
function buildPrvText(blocks) {
|
|
20498
|
+
const lines = [];
|
|
20499
|
+
let bytes = 0;
|
|
20500
|
+
for (const b of blocks) {
|
|
20501
|
+
let text = b.text || (b.rows ? b.rows.map((r) => r.join(" ")).join("\n") : "");
|
|
20502
|
+
if (b.type === "html_table") text = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
20503
|
+
if (!text) continue;
|
|
20504
|
+
lines.push(text);
|
|
20505
|
+
bytes += text.length * 3;
|
|
20506
|
+
if (bytes > 1024) break;
|
|
20427
20507
|
}
|
|
20428
|
-
return
|
|
20508
|
+
return lines.join("\n").slice(0, 1024);
|
|
20429
20509
|
}
|
|
20430
|
-
|
|
20431
|
-
// src/roundtrip/markdown-units.ts
|
|
20432
|
-
function splitMarkdownUnits(md2) {
|
|
20510
|
+
function parseMarkdownToBlocks(md2) {
|
|
20433
20511
|
const lines = md2.split("\n");
|
|
20434
|
-
const
|
|
20512
|
+
const blocks = [];
|
|
20435
20513
|
let i = 0;
|
|
20436
20514
|
while (i < lines.length) {
|
|
20437
20515
|
const line = lines[i];
|
|
@@ -20439,442 +20517,451 @@ function splitMarkdownUnits(md2) {
|
|
|
20439
20517
|
i++;
|
|
20440
20518
|
continue;
|
|
20441
20519
|
}
|
|
20442
|
-
|
|
20443
|
-
|
|
20520
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(.*)$/);
|
|
20521
|
+
if (fenceMatch) {
|
|
20522
|
+
const fence = fenceMatch[1];
|
|
20523
|
+
const lang = fenceMatch[2].trim();
|
|
20524
|
+
const codeLines = [];
|
|
20525
|
+
i++;
|
|
20526
|
+
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
20527
|
+
codeLines.push(lines[i]);
|
|
20528
|
+
i++;
|
|
20529
|
+
}
|
|
20530
|
+
if (i < lines.length) i++;
|
|
20531
|
+
blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
|
|
20532
|
+
continue;
|
|
20533
|
+
}
|
|
20534
|
+
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
20535
|
+
blocks.push({ type: "hr" });
|
|
20536
|
+
i++;
|
|
20537
|
+
continue;
|
|
20538
|
+
}
|
|
20539
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
20540
|
+
if (headingMatch) {
|
|
20541
|
+
blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
|
|
20542
|
+
i++;
|
|
20543
|
+
continue;
|
|
20544
|
+
}
|
|
20545
|
+
if (/^<table[\s>]/i.test(line.trimStart())) {
|
|
20546
|
+
const htmlLines = [];
|
|
20444
20547
|
let depth = 0;
|
|
20445
20548
|
while (i < lines.length) {
|
|
20446
20549
|
const l = lines[i];
|
|
20447
|
-
|
|
20448
|
-
depth += (l.match(/<table
|
|
20449
|
-
depth -= (l.match(/<\/table>/
|
|
20550
|
+
htmlLines.push(l);
|
|
20551
|
+
depth += (l.match(/<table[\s>]/gi) ?? []).length;
|
|
20552
|
+
depth -= (l.match(/<\/table>/gi) ?? []).length;
|
|
20450
20553
|
i++;
|
|
20451
20554
|
if (depth <= 0) break;
|
|
20452
20555
|
}
|
|
20453
|
-
|
|
20556
|
+
blocks.push({ type: "html_table", text: htmlLines.join("\n") });
|
|
20454
20557
|
continue;
|
|
20455
20558
|
}
|
|
20456
20559
|
if (line.trimStart().startsWith("|")) {
|
|
20457
|
-
const
|
|
20560
|
+
const tableRows = [];
|
|
20458
20561
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
20459
|
-
|
|
20562
|
+
const row = lines[i];
|
|
20563
|
+
if (/^[\s|:\-]+$/.test(row)) {
|
|
20564
|
+
i++;
|
|
20565
|
+
continue;
|
|
20566
|
+
}
|
|
20567
|
+
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
20568
|
+
if (cells.length > 0) tableRows.push(cells);
|
|
20460
20569
|
i++;
|
|
20461
20570
|
}
|
|
20462
|
-
|
|
20571
|
+
if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
|
|
20463
20572
|
continue;
|
|
20464
20573
|
}
|
|
20465
|
-
if (
|
|
20466
|
-
|
|
20467
|
-
i
|
|
20574
|
+
if (line.trimStart().startsWith("> ")) {
|
|
20575
|
+
const quoteLines = [];
|
|
20576
|
+
while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
|
|
20577
|
+
quoteLines.push(lines[i].replace(/^>\s?/, ""));
|
|
20578
|
+
i++;
|
|
20579
|
+
}
|
|
20580
|
+
for (const ql of quoteLines) {
|
|
20581
|
+
blocks.push({ type: "blockquote", text: ql.trim() || "" });
|
|
20582
|
+
}
|
|
20468
20583
|
continue;
|
|
20469
20584
|
}
|
|
20470
|
-
|
|
20471
|
-
|
|
20585
|
+
const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
|
|
20586
|
+
if (listMatch) {
|
|
20587
|
+
const indent = Math.floor(listMatch[1].length / 2);
|
|
20588
|
+
const ordered = /\d/.test(listMatch[2]);
|
|
20589
|
+
blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
|
|
20472
20590
|
i++;
|
|
20473
20591
|
continue;
|
|
20474
20592
|
}
|
|
20475
|
-
|
|
20476
|
-
|
|
20477
|
-
collected.push(lines[i].trim());
|
|
20478
|
-
i++;
|
|
20479
|
-
}
|
|
20480
|
-
units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
|
|
20593
|
+
blocks.push({ type: "paragraph", text: line.trim() });
|
|
20594
|
+
i++;
|
|
20481
20595
|
}
|
|
20482
|
-
return
|
|
20596
|
+
return blocks;
|
|
20483
20597
|
}
|
|
20484
|
-
function
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20598
|
+
function parseInlineMarkdown(text) {
|
|
20599
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
20600
|
+
text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
|
|
20601
|
+
text = text.replace(/~~([^~]+)~~/g, "$1");
|
|
20602
|
+
const spans = [];
|
|
20603
|
+
const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
|
|
20604
|
+
let lastIdx = 0;
|
|
20605
|
+
for (const match of text.matchAll(regex)) {
|
|
20606
|
+
const idx = match.index;
|
|
20607
|
+
if (idx > lastIdx) {
|
|
20608
|
+
spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
|
|
20492
20609
|
}
|
|
20493
|
-
|
|
20494
|
-
|
|
20495
|
-
|
|
20496
|
-
if (
|
|
20497
|
-
|
|
20610
|
+
const raw = match[0];
|
|
20611
|
+
if (raw.startsWith("`")) {
|
|
20612
|
+
spans.push({ text: raw.slice(1, -1), bold: false, italic: false, code: true });
|
|
20613
|
+
} else if (raw.startsWith("***") || raw.startsWith("___")) {
|
|
20614
|
+
spans.push({ text: raw.slice(3, -3), bold: true, italic: true, code: false });
|
|
20615
|
+
} else if (raw.startsWith("**") || raw.startsWith("__")) {
|
|
20616
|
+
spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
|
|
20498
20617
|
} else {
|
|
20499
|
-
|
|
20500
|
-
for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
|
|
20501
|
-
}
|
|
20502
|
-
for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
|
|
20503
|
-
return result2;
|
|
20504
|
-
}
|
|
20505
|
-
const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
|
|
20506
|
-
for (let i2 = 1; i2 <= m; i2++) {
|
|
20507
|
-
for (let j2 = 1; j2 <= n; j2++) {
|
|
20508
|
-
dp[i2][j2] = a[i2 - 1] === b[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
20509
|
-
}
|
|
20510
|
-
}
|
|
20511
|
-
const matches = [];
|
|
20512
|
-
let i = m, j = n;
|
|
20513
|
-
while (i > 0 && j > 0) {
|
|
20514
|
-
if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
20515
|
-
matches.push([i - 1, j - 1]);
|
|
20516
|
-
i--;
|
|
20517
|
-
j--;
|
|
20518
|
-
} else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
|
|
20519
|
-
else j--;
|
|
20520
|
-
}
|
|
20521
|
-
matches.reverse();
|
|
20522
|
-
const result = [];
|
|
20523
|
-
let ai = 0, bi = 0;
|
|
20524
|
-
const flushGap = (aEnd, bEnd) => {
|
|
20525
|
-
if (aEnd - ai === bEnd - bi) {
|
|
20526
|
-
while (ai < aEnd) result.push([ai++, bi++]);
|
|
20527
|
-
return;
|
|
20528
|
-
}
|
|
20529
|
-
while (ai < aEnd && bi < bEnd) {
|
|
20530
|
-
const sim = normalizedSimilarity(a[ai], b[bi]);
|
|
20531
|
-
if (sim >= 0.4) {
|
|
20532
|
-
if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
|
|
20533
|
-
result.push([ai++, null]);
|
|
20534
|
-
} else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
|
|
20535
|
-
result.push([null, bi++]);
|
|
20536
|
-
} else {
|
|
20537
|
-
result.push([ai++, bi++]);
|
|
20538
|
-
}
|
|
20539
|
-
} else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
|
|
20540
|
-
else result.push([null, bi++]);
|
|
20541
|
-
}
|
|
20542
|
-
while (ai < aEnd) result.push([ai++, null]);
|
|
20543
|
-
while (bi < bEnd) result.push([null, bi++]);
|
|
20544
|
-
};
|
|
20545
|
-
for (const [pi, pj] of matches) {
|
|
20546
|
-
flushGap(pi, pj);
|
|
20547
|
-
result.push([ai++, bi++]);
|
|
20548
|
-
}
|
|
20549
|
-
flushGap(m, n);
|
|
20550
|
-
return result;
|
|
20551
|
-
}
|
|
20552
|
-
function bestSimInRange(arr, from, to, target) {
|
|
20553
|
-
let best = 0;
|
|
20554
|
-
for (let k = from; k <= to && k < arr.length; k++) {
|
|
20555
|
-
const s = normalizedSimilarity(arr[k], target);
|
|
20556
|
-
if (s > best) best = s;
|
|
20557
|
-
}
|
|
20558
|
-
return best;
|
|
20559
|
-
}
|
|
20560
|
-
function escapeGfm(text) {
|
|
20561
|
-
return text.replace(/~/g, "\\~");
|
|
20562
|
-
}
|
|
20563
|
-
var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
|
|
20564
|
-
function sanitizeText(text) {
|
|
20565
|
-
let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
|
|
20566
|
-
if (result.length <= 30 && result.includes(" ")) {
|
|
20567
|
-
const tokens = result.split(" ");
|
|
20568
|
-
const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-ㄱ-ㆎ]/.test(t)).length;
|
|
20569
|
-
if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
|
|
20570
|
-
result = tokens.join("");
|
|
20618
|
+
spans.push({ text: raw.slice(1, -1), bold: false, italic: true, code: false });
|
|
20571
20619
|
}
|
|
20620
|
+
lastIdx = idx + raw.length;
|
|
20572
20621
|
}
|
|
20573
|
-
|
|
20574
|
-
}
|
|
20575
|
-
function normForMatch(text) {
|
|
20576
|
-
return sanitizeText(text).replace(/\s+/g, " ").trim();
|
|
20577
|
-
}
|
|
20578
|
-
function unescapeGfm(text) {
|
|
20579
|
-
return text.replace(/\\~/g, "~");
|
|
20580
|
-
}
|
|
20581
|
-
function summarize(text) {
|
|
20582
|
-
const t = text.replace(/\s+/g, " ").trim();
|
|
20583
|
-
return t.length > 80 ? t.slice(0, 77) + "..." : t;
|
|
20584
|
-
}
|
|
20585
|
-
function replicateGfmTable(table) {
|
|
20586
|
-
const { cells, rows: numRows, cols: numCols } = table;
|
|
20587
|
-
if (numRows === 0 || numCols === 0) return null;
|
|
20588
|
-
if (numRows === 1 && numCols === 1) return null;
|
|
20589
|
-
if (numCols === 1) return null;
|
|
20590
|
-
const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
|
|
20591
|
-
const skip = /* @__PURE__ */ new Set();
|
|
20592
|
-
for (let r = 0; r < numRows; r++) {
|
|
20593
|
-
for (let c = 0; c < numCols; c++) {
|
|
20594
|
-
if (skip.has(`${r},${c}`)) continue;
|
|
20595
|
-
const cell = cells[r]?.[c];
|
|
20596
|
-
if (!cell) continue;
|
|
20597
|
-
display[r][c] = {
|
|
20598
|
-
text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
|
|
20599
|
-
gridR: r,
|
|
20600
|
-
gridC: c
|
|
20601
|
-
};
|
|
20602
|
-
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
20603
|
-
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
20604
|
-
if (dr === 0 && dc === 0) continue;
|
|
20605
|
-
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
20606
|
-
}
|
|
20607
|
-
}
|
|
20608
|
-
c += cell.colSpan - 1;
|
|
20609
|
-
}
|
|
20622
|
+
if (lastIdx < text.length) {
|
|
20623
|
+
spans.push({ text: text.slice(lastIdx), bold: false, italic: false, code: false });
|
|
20610
20624
|
}
|
|
20611
|
-
|
|
20612
|
-
|
|
20613
|
-
for (let r = 0; r < display.length; r++) {
|
|
20614
|
-
const row = display[r];
|
|
20615
|
-
if (row.every((cell) => cell.text === "")) continue;
|
|
20616
|
-
const nonEmptyCols = row.filter((cell) => cell.text !== "");
|
|
20617
|
-
const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
|
|
20618
|
-
if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
|
|
20619
|
-
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
20620
|
-
pendingLabelRow = row;
|
|
20621
|
-
continue;
|
|
20622
|
-
}
|
|
20623
|
-
if (pendingLabelRow) {
|
|
20624
|
-
if (row[0].text === "") row[0] = pendingLabelRow[0];
|
|
20625
|
-
else uniqueRows.push(pendingLabelRow);
|
|
20626
|
-
pendingLabelRow = null;
|
|
20627
|
-
}
|
|
20628
|
-
uniqueRows.push(row);
|
|
20625
|
+
if (spans.length === 0) {
|
|
20626
|
+
spans.push({ text, bold: false, italic: false, code: false });
|
|
20629
20627
|
}
|
|
20630
|
-
|
|
20631
|
-
return uniqueRows.length > 0 ? uniqueRows : null;
|
|
20628
|
+
return spans;
|
|
20632
20629
|
}
|
|
20633
|
-
function
|
|
20634
|
-
|
|
20635
|
-
|
|
20636
|
-
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
if (cells.length === 0) continue;
|
|
20640
|
-
if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
|
20641
|
-
rows.push(cells);
|
|
20642
|
-
}
|
|
20643
|
-
return rows;
|
|
20630
|
+
function spanToCharPrId(span) {
|
|
20631
|
+
if (span.code) return CHAR_CODE;
|
|
20632
|
+
if (span.bold && span.italic) return CHAR_BOLD_ITALIC;
|
|
20633
|
+
if (span.bold) return CHAR_BOLD;
|
|
20634
|
+
if (span.italic) return CHAR_ITALIC;
|
|
20635
|
+
return CHAR_NORMAL;
|
|
20644
20636
|
}
|
|
20645
|
-
function
|
|
20646
|
-
|
|
20637
|
+
function generateRuns(text, defaultCharPr = CHAR_NORMAL, mapCharId) {
|
|
20638
|
+
const spans = parseInlineMarkdown(text);
|
|
20639
|
+
return spans.map((span) => {
|
|
20640
|
+
let charId = span.code || span.bold || span.italic ? spanToCharPrId(span) : defaultCharPr;
|
|
20641
|
+
if (mapCharId) charId = mapCharId(charId);
|
|
20642
|
+
return `<hp:run charPrIDRef="${charId}"><hp:t>${escapeXml(span.text)}</hp:t></hp:run>`;
|
|
20643
|
+
}).join("");
|
|
20647
20644
|
}
|
|
20648
|
-
function
|
|
20649
|
-
if (
|
|
20650
|
-
return
|
|
20651
|
-
if (b.type === "table" && b.table) {
|
|
20652
|
-
const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
|
|
20653
|
-
return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
|
|
20654
|
-
}
|
|
20655
|
-
if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
|
|
20656
|
-
const t = sanitizeText(b.text ?? "");
|
|
20657
|
-
return t ? t.replace(/\n/g, "<br>") : "";
|
|
20658
|
-
}).filter(Boolean).join("<br>");
|
|
20645
|
+
function generateParagraph(text, paraPrId = PARA_NORMAL, charPrId = CHAR_NORMAL, mapCharId) {
|
|
20646
|
+
if (paraPrId === PARA_CODE) {
|
|
20647
|
+
return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_CODE}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
|
|
20659
20648
|
}
|
|
20660
|
-
|
|
20649
|
+
const runs = generateRuns(text, charPrId, mapCharId);
|
|
20650
|
+
return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
|
|
20661
20651
|
}
|
|
20662
|
-
|
|
20663
|
-
|
|
20664
|
-
|
|
20665
|
-
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
|
|
20652
|
+
|
|
20653
|
+
// src/hwpx/gen-header.ts
|
|
20654
|
+
function generateContainerXml() {
|
|
20655
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20656
|
+
<ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
|
|
20657
|
+
<ocf:rootfiles>
|
|
20658
|
+
<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
|
|
20659
|
+
</ocf:rootfiles>
|
|
20660
|
+
</ocf:container>`;
|
|
20661
|
+
}
|
|
20662
|
+
function generateManifest() {
|
|
20663
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20664
|
+
<opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
|
|
20665
|
+
<opf:manifest>
|
|
20666
|
+
<opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
|
|
20667
|
+
<opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>
|
|
20668
|
+
</opf:manifest>
|
|
20669
|
+
<opf:spine>
|
|
20670
|
+
<opf:itemref idref="header" linear="no"/>
|
|
20671
|
+
<opf:itemref idref="section0" linear="yes"/>
|
|
20672
|
+
</opf:spine>
|
|
20673
|
+
</opf:package>`;
|
|
20674
|
+
}
|
|
20675
|
+
function buildCharProperties(theme, gongmun, ratioVariants = []) {
|
|
20676
|
+
let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
|
|
20677
|
+
if (gongmun) {
|
|
20678
|
+
body = gongmun.bodyHeight;
|
|
20679
|
+
code = Math.max(body - 200, 900);
|
|
20680
|
+
h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
|
|
20681
|
+
h2 = 1600;
|
|
20682
|
+
h3 = body;
|
|
20683
|
+
h4 = Math.max(body - 100, 1300);
|
|
20675
20684
|
}
|
|
20676
|
-
|
|
20677
|
-
|
|
20685
|
+
const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
|
|
20686
|
+
const rows = [
|
|
20687
|
+
charPr(0, body, false, false, 0, theme.body, bodyRatio),
|
|
20688
|
+
charPr(1, body, true, false, 0, theme.body, bodyRatio),
|
|
20689
|
+
charPr(2, body, false, true, 0, theme.body, bodyRatio),
|
|
20690
|
+
charPr(3, body, true, true, 0, theme.body, bodyRatio),
|
|
20691
|
+
charPr(4, code, false, false, 1),
|
|
20692
|
+
charPr(5, h1, true, false, 1, theme.h1),
|
|
20693
|
+
charPr(6, h2, true, false, 1, theme.h2),
|
|
20694
|
+
charPr(7, h3, true, false, 1, theme.h3),
|
|
20695
|
+
charPr(8, h4, true, false, 1, theme.h4),
|
|
20696
|
+
charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, 0, theme.tableHeader),
|
|
20697
|
+
charPr(CHAR_QUOTE, body, false, true, 0, theme.quote)
|
|
20698
|
+
];
|
|
20699
|
+
for (const r of ratioVariants) {
|
|
20700
|
+
rows.push(
|
|
20701
|
+
charPr(rows.length, body, false, false, 0, theme.body, r),
|
|
20702
|
+
charPr(rows.length + 1, body, true, false, 0, theme.body, r),
|
|
20703
|
+
charPr(rows.length + 2, body, false, true, 0, theme.body, r),
|
|
20704
|
+
charPr(rows.length + 3, body, true, true, 0, theme.body, r)
|
|
20705
|
+
);
|
|
20706
|
+
}
|
|
20707
|
+
return `<hh:charProperties itemCnt="${rows.length}">
|
|
20708
|
+
${rows.join("\n")}
|
|
20709
|
+
</hh:charProperties>`;
|
|
20678
20710
|
}
|
|
20679
|
-
function
|
|
20680
|
-
|
|
20681
|
-
|
|
20682
|
-
|
|
20683
|
-
|
|
20684
|
-
|
|
20685
|
-
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
|
|
20694
|
-
}
|
|
20695
|
-
}
|
|
20696
|
-
rowCells.push({
|
|
20697
|
-
inner: replicateCellInnerHtml(cell),
|
|
20698
|
-
colSpan: cell.colSpan,
|
|
20699
|
-
rowSpan: cell.rowSpan,
|
|
20700
|
-
gridR: r,
|
|
20701
|
-
gridC: c
|
|
20702
|
-
});
|
|
20703
|
-
}
|
|
20704
|
-
if (rowCells.length) result.push({ tag, cells: rowCells });
|
|
20711
|
+
function buildParaProperties(gongmun) {
|
|
20712
|
+
if (!gongmun) {
|
|
20713
|
+
const base2 = [
|
|
20714
|
+
paraPr(0),
|
|
20715
|
+
paraPr(1, { align: "LEFT", spaceBefore: 800, spaceAfter: 200, lineSpacing: 180 }),
|
|
20716
|
+
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: 170 }),
|
|
20717
|
+
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: 160 }),
|
|
20718
|
+
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
|
|
20719
|
+
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
|
|
20720
|
+
paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
|
|
20721
|
+
paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
|
|
20722
|
+
];
|
|
20723
|
+
return `<hh:paraProperties itemCnt="${base2.length}">
|
|
20724
|
+
${base2.join("\n")}
|
|
20725
|
+
</hh:paraProperties>`;
|
|
20705
20726
|
}
|
|
20706
|
-
|
|
20727
|
+
const ls = gongmun.lineSpacing;
|
|
20728
|
+
const titleAlign = gongmun.centerTitle ? "CENTER" : "LEFT";
|
|
20729
|
+
const base = [
|
|
20730
|
+
paraPr(0, { lineSpacing: ls, keepWord: true }),
|
|
20731
|
+
paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
|
|
20732
|
+
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
|
|
20733
|
+
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
20734
|
+
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
20735
|
+
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
|
|
20736
|
+
paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
|
|
20737
|
+
paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
|
|
20738
|
+
];
|
|
20739
|
+
for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
|
|
20740
|
+
const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
|
|
20741
|
+
const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
|
|
20742
|
+
base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
|
|
20743
|
+
}
|
|
20744
|
+
base.push(paraPr(GONGMUN_CENTER, { align: "CENTER", lineSpacing: ls, keepWord: true }));
|
|
20745
|
+
return `<hh:paraProperties itemCnt="${base.length}">
|
|
20746
|
+
${base.join("\n")}
|
|
20747
|
+
</hh:paraProperties>`;
|
|
20748
|
+
}
|
|
20749
|
+
function generateHeaderXml(theme, gongmun, ratioVariants = []) {
|
|
20750
|
+
const bodyFace = gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5";
|
|
20751
|
+
const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants);
|
|
20752
|
+
const paraPropsXml = buildParaProperties(gongmun);
|
|
20753
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20754
|
+
<hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
|
|
20755
|
+
<hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
|
|
20756
|
+
<hh:refList>
|
|
20757
|
+
<hh:fontfaces itemCnt="7">
|
|
20758
|
+
<hh:fontface lang="HANGUL" fontCnt="3">
|
|
20759
|
+
<hh:font id="0" face="${bodyFace}" type="TTF" isEmbedded="0">
|
|
20760
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20761
|
+
</hh:font>
|
|
20762
|
+
<hh:font id="1" face="\uD568\uCD08\uB86C\uB3CB\uC6C0" type="TTF" isEmbedded="0">
|
|
20763
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20764
|
+
</hh:font>
|
|
20765
|
+
<hh:font id="2" face="HY\uACAC\uACE0\uB515" type="TTF" isEmbedded="0">
|
|
20766
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20767
|
+
</hh:font>
|
|
20768
|
+
</hh:fontface>
|
|
20769
|
+
<hh:fontface lang="LATIN" fontCnt="3">
|
|
20770
|
+
<hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
|
|
20771
|
+
<hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
|
|
20772
|
+
</hh:font>
|
|
20773
|
+
<hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
|
|
20774
|
+
<hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
20775
|
+
</hh:font>
|
|
20776
|
+
<hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
|
|
20777
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
20778
|
+
</hh:font>
|
|
20779
|
+
</hh:fontface>
|
|
20780
|
+
<hh:fontface lang="HANJA" fontCnt="1">
|
|
20781
|
+
<hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
|
|
20782
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20783
|
+
</hh:font>
|
|
20784
|
+
</hh:fontface>
|
|
20785
|
+
<hh:fontface lang="JAPANESE" fontCnt="1">
|
|
20786
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
20787
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20788
|
+
</hh:font>
|
|
20789
|
+
</hh:fontface>
|
|
20790
|
+
<hh:fontface lang="OTHER" fontCnt="1">
|
|
20791
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
20792
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20793
|
+
</hh:font>
|
|
20794
|
+
</hh:fontface>
|
|
20795
|
+
<hh:fontface lang="SYMBOL" fontCnt="1">
|
|
20796
|
+
<hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
|
|
20797
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20798
|
+
</hh:font>
|
|
20799
|
+
</hh:fontface>
|
|
20800
|
+
<hh:fontface lang="USER" fontCnt="1">
|
|
20801
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
20802
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20803
|
+
</hh:font>
|
|
20804
|
+
</hh:fontface>
|
|
20805
|
+
</hh:fontfaces>
|
|
20806
|
+
<hh:borderFills itemCnt="2">
|
|
20807
|
+
<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
20808
|
+
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
20809
|
+
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
20810
|
+
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
20811
|
+
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
20812
|
+
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
20813
|
+
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
20814
|
+
</hh:borderFill>
|
|
20815
|
+
<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
20816
|
+
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
20817
|
+
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
20818
|
+
<hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
20819
|
+
<hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
20820
|
+
<hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
20821
|
+
<hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
20822
|
+
</hh:borderFill>
|
|
20823
|
+
</hh:borderFills>
|
|
20824
|
+
${charPropsXml}
|
|
20825
|
+
<hh:tabProperties itemCnt="0"/>
|
|
20826
|
+
<hh:numberings itemCnt="0"/>
|
|
20827
|
+
<hh:bullets itemCnt="0"/>
|
|
20828
|
+
${paraPropsXml}
|
|
20829
|
+
<hh:styles itemCnt="1">
|
|
20830
|
+
<hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
|
|
20831
|
+
</hh:styles>
|
|
20832
|
+
</hh:refList>
|
|
20833
|
+
<hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
|
|
20834
|
+
</hh:head>`;
|
|
20707
20835
|
}
|
|
20708
|
-
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
const
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20730
|
-
|
|
20731
|
-
|
|
20836
|
+
|
|
20837
|
+
// src/hwpx/gen-gongmun-fit.ts
|
|
20838
|
+
function plainRenderText(text) {
|
|
20839
|
+
return parseInlineMarkdown(text).map((s) => s.text).join("");
|
|
20840
|
+
}
|
|
20841
|
+
function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
|
|
20842
|
+
const minRatio = gongmun.autoFitMinRatio;
|
|
20843
|
+
if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
|
|
20844
|
+
const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
|
|
20845
|
+
const ratioByBlock = /* @__PURE__ */ new Map();
|
|
20846
|
+
const variants = [];
|
|
20847
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
20848
|
+
const block = blocks[i];
|
|
20849
|
+
let text;
|
|
20850
|
+
let firstW;
|
|
20851
|
+
let contW;
|
|
20852
|
+
if (block.type === "list_item" && gongmunList.has(i)) {
|
|
20853
|
+
const { marker, depth } = gongmunList.get(i);
|
|
20854
|
+
const content = plainRenderText(block.text || "");
|
|
20855
|
+
text = marker ? `${marker} ${content}` : content;
|
|
20856
|
+
const { left, indent } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering);
|
|
20857
|
+
firstW = pageW - left - Math.max(indent, 0);
|
|
20858
|
+
contW = pageW - left - Math.max(-indent, 0);
|
|
20859
|
+
} else if (block.type === "paragraph") {
|
|
20860
|
+
const raw = (block.text || "").trim();
|
|
20861
|
+
if (/^<center>[\s\S]*<\/center>$/i.test(raw)) continue;
|
|
20862
|
+
text = plainRenderText(raw);
|
|
20863
|
+
firstW = contW = pageW;
|
|
20732
20864
|
} else {
|
|
20733
|
-
|
|
20734
|
-
const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
20735
|
-
const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
20736
|
-
cellStart = m.index + m[0].length;
|
|
20737
|
-
cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
|
|
20738
|
-
} else if (cellStart >= 0 && cellInfo && currentRow) {
|
|
20739
|
-
currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
|
|
20740
|
-
cellStart = -1;
|
|
20741
|
-
cellInfo = null;
|
|
20742
|
-
}
|
|
20865
|
+
continue;
|
|
20743
20866
|
}
|
|
20867
|
+
if (!text) continue;
|
|
20868
|
+
const r = fitRatioForFewerLines(text, firstW, contW, gongmun.bodyHeight, GONGMUN_BODY_RATIO, minRatio);
|
|
20869
|
+
if (r === null) continue;
|
|
20870
|
+
ratioByBlock.set(i, r);
|
|
20871
|
+
if (!variants.includes(r)) variants.push(r);
|
|
20744
20872
|
}
|
|
20745
|
-
|
|
20746
|
-
return rows;
|
|
20873
|
+
return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
|
|
20747
20874
|
}
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
hadNonText = true;
|
|
20754
|
-
work = removeNestedTables(work);
|
|
20755
|
-
}
|
|
20756
|
-
if (/<img\s/i.test(work)) {
|
|
20757
|
-
hadNonText = true;
|
|
20758
|
-
work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
|
|
20759
|
-
}
|
|
20760
|
-
const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
20761
|
-
return { lines, hadNonText };
|
|
20875
|
+
function variantMapper(fit, blockIdx) {
|
|
20876
|
+
const r = fit.ratioByBlock.get(blockIdx);
|
|
20877
|
+
if (r === void 0) return void 0;
|
|
20878
|
+
const vi = fit.variants.indexOf(r);
|
|
20879
|
+
return (id) => id >= 0 && id <= 3 ? CHAR_VARIANT_BASE + vi * 4 + id : id;
|
|
20762
20880
|
}
|
|
20763
|
-
function
|
|
20764
|
-
const result =
|
|
20765
|
-
let
|
|
20766
|
-
|
|
20767
|
-
|
|
20768
|
-
|
|
20769
|
-
|
|
20770
|
-
if (m[1] !== "/") {
|
|
20771
|
-
if (depth === 0) start = m.index;
|
|
20772
|
-
depth++;
|
|
20773
|
-
} else {
|
|
20774
|
-
depth--;
|
|
20775
|
-
if (depth === 0 && start >= 0) {
|
|
20776
|
-
result.push(html.slice(start, m.index + m[0].length));
|
|
20777
|
-
start = -1;
|
|
20778
|
-
}
|
|
20779
|
-
if (depth < 0) depth = 0;
|
|
20881
|
+
function precomputeGongmunList(blocks, gongmun) {
|
|
20882
|
+
const result = /* @__PURE__ */ new Map();
|
|
20883
|
+
let i = 0;
|
|
20884
|
+
while (i < blocks.length) {
|
|
20885
|
+
if (blocks[i].type !== "list_item") {
|
|
20886
|
+
i++;
|
|
20887
|
+
continue;
|
|
20780
20888
|
}
|
|
20781
|
-
|
|
20782
|
-
|
|
20783
|
-
|
|
20784
|
-
|
|
20785
|
-
|
|
20786
|
-
|
|
20787
|
-
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
20797
|
-
|
|
20889
|
+
const run = [];
|
|
20890
|
+
while (i < blocks.length) {
|
|
20891
|
+
const t = blocks[i].type;
|
|
20892
|
+
if (t === "list_item") {
|
|
20893
|
+
run.push(i);
|
|
20894
|
+
i++;
|
|
20895
|
+
continue;
|
|
20896
|
+
}
|
|
20897
|
+
if (t === "table" || t === "html_table") {
|
|
20898
|
+
let j = i + 1;
|
|
20899
|
+
while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
|
|
20900
|
+
if (j < blocks.length && blocks[j].type === "list_item") {
|
|
20901
|
+
i = j;
|
|
20902
|
+
continue;
|
|
20903
|
+
}
|
|
20904
|
+
}
|
|
20905
|
+
break;
|
|
20798
20906
|
}
|
|
20907
|
+
const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
|
|
20908
|
+
const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
|
|
20909
|
+
const numberer = new GongmunNumberer(gongmun.numbering);
|
|
20910
|
+
run.forEach((bi, k) => {
|
|
20911
|
+
const marker = numberer.next(depths[k], suppress[k]);
|
|
20912
|
+
result.set(bi, { marker, depth: depths[k] });
|
|
20913
|
+
});
|
|
20799
20914
|
}
|
|
20800
|
-
if (depth === 0) result += html.slice(last);
|
|
20801
20915
|
return result;
|
|
20802
20916
|
}
|
|
20803
20917
|
|
|
20804
|
-
// src/
|
|
20805
|
-
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
|
|
20811
|
-
var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
|
|
20812
|
-
var CHAR_NORMAL = 0;
|
|
20813
|
-
var CHAR_BOLD = 1;
|
|
20814
|
-
var CHAR_ITALIC = 2;
|
|
20815
|
-
var CHAR_BOLD_ITALIC = 3;
|
|
20816
|
-
var CHAR_CODE = 4;
|
|
20817
|
-
var CHAR_H1 = 5;
|
|
20818
|
-
var CHAR_H2 = 6;
|
|
20819
|
-
var CHAR_H3 = 7;
|
|
20820
|
-
var CHAR_H4 = 8;
|
|
20821
|
-
var CHAR_TABLE_HEADER = 9;
|
|
20822
|
-
var CHAR_QUOTE = 10;
|
|
20823
|
-
var PARA_NORMAL = 0;
|
|
20824
|
-
var PARA_H1 = 1;
|
|
20825
|
-
var PARA_H2 = 2;
|
|
20826
|
-
var PARA_H3 = 3;
|
|
20827
|
-
var PARA_H4 = 4;
|
|
20828
|
-
var PARA_CODE = 5;
|
|
20829
|
-
var PARA_QUOTE = 6;
|
|
20830
|
-
var PARA_LIST = 7;
|
|
20831
|
-
var DEFAULT_TEXT_COLOR = "#000000";
|
|
20832
|
-
function resolveTheme(theme) {
|
|
20833
|
-
return {
|
|
20834
|
-
h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
|
|
20835
|
-
h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
|
|
20836
|
-
h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20837
|
-
h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20838
|
-
body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20839
|
-
quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
|
|
20840
|
-
/** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
|
|
20841
|
-
hasQuoteOption: theme?.quoteColor !== void 0,
|
|
20842
|
-
tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20843
|
-
tableHeaderBold: !!theme?.tableHeaderBold
|
|
20844
|
-
};
|
|
20918
|
+
// src/diff/text-diff.ts
|
|
20919
|
+
function similarity(a, b) {
|
|
20920
|
+
if (a === b) return 1;
|
|
20921
|
+
if (!a || !b) return 0;
|
|
20922
|
+
const maxLen = Math.max(a.length, b.length);
|
|
20923
|
+
if (maxLen === 0) return 1;
|
|
20924
|
+
return 1 - levenshtein(a, b) / maxLen;
|
|
20845
20925
|
}
|
|
20846
|
-
|
|
20847
|
-
|
|
20848
|
-
const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
|
|
20849
|
-
const blocks = parseMarkdownToBlocks(markdown);
|
|
20850
|
-
const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
|
|
20851
|
-
const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
|
|
20852
|
-
const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
|
|
20853
|
-
const zip = new JSZip5();
|
|
20854
|
-
zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
|
|
20855
|
-
zip.file("META-INF/container.xml", generateContainerXml());
|
|
20856
|
-
zip.file("Contents/content.hpf", generateManifest());
|
|
20857
|
-
zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
|
|
20858
|
-
zip.file("Contents/section0.xml", sectionXml);
|
|
20859
|
-
zip.file("Preview/PrvText.txt", buildPrvText(blocks));
|
|
20860
|
-
return await zip.generateAsync({ type: "arraybuffer" });
|
|
20926
|
+
function normalizedSimilarity(a, b) {
|
|
20927
|
+
return similarity(normalize(a), normalize(b));
|
|
20861
20928
|
}
|
|
20862
|
-
function
|
|
20863
|
-
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
20929
|
+
function normalize(s) {
|
|
20930
|
+
return s.replace(/\s+/g, " ").trim();
|
|
20931
|
+
}
|
|
20932
|
+
var MAX_LEVENSHTEIN_LEN = 1e4;
|
|
20933
|
+
function levenshtein(a, b) {
|
|
20934
|
+
if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
|
|
20935
|
+
const sampleLen = Math.min(500, a.length, b.length);
|
|
20936
|
+
let diffs = 0;
|
|
20937
|
+
for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
|
|
20938
|
+
const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
|
|
20939
|
+
return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
|
|
20940
|
+
}
|
|
20941
|
+
if (a.length > b.length) [a, b] = [b, a];
|
|
20942
|
+
const m = a.length;
|
|
20943
|
+
const n = b.length;
|
|
20944
|
+
let prev = Array.from({ length: m + 1 }, (_, i) => i);
|
|
20945
|
+
let curr = new Array(m + 1);
|
|
20946
|
+
for (let j = 1; j <= n; j++) {
|
|
20947
|
+
curr[0] = j;
|
|
20948
|
+
for (let i = 1; i <= m; i++) {
|
|
20949
|
+
if (a[i - 1] === b[j - 1]) {
|
|
20950
|
+
curr[i] = prev[i - 1];
|
|
20951
|
+
} else {
|
|
20952
|
+
curr[i] = 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
|
|
20953
|
+
}
|
|
20954
|
+
}
|
|
20955
|
+
;
|
|
20956
|
+
[prev, curr] = [curr, prev];
|
|
20872
20957
|
}
|
|
20873
|
-
return
|
|
20958
|
+
return prev[m];
|
|
20874
20959
|
}
|
|
20875
|
-
|
|
20960
|
+
|
|
20961
|
+
// src/roundtrip/markdown-units.ts
|
|
20962
|
+
function splitMarkdownUnits(md2) {
|
|
20876
20963
|
const lines = md2.split("\n");
|
|
20877
|
-
const
|
|
20964
|
+
const units = [];
|
|
20878
20965
|
let i = 0;
|
|
20879
20966
|
while (i < lines.length) {
|
|
20880
20967
|
const line = lines[i];
|
|
@@ -20882,420 +20969,369 @@ function parseMarkdownToBlocks(md2) {
|
|
|
20882
20969
|
i++;
|
|
20883
20970
|
continue;
|
|
20884
20971
|
}
|
|
20885
|
-
|
|
20886
|
-
|
|
20887
|
-
const fence = fenceMatch[1];
|
|
20888
|
-
const lang = fenceMatch[2].trim();
|
|
20889
|
-
const codeLines = [];
|
|
20890
|
-
i++;
|
|
20891
|
-
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
20892
|
-
codeLines.push(lines[i]);
|
|
20893
|
-
i++;
|
|
20894
|
-
}
|
|
20895
|
-
if (i < lines.length) i++;
|
|
20896
|
-
blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
|
|
20897
|
-
continue;
|
|
20898
|
-
}
|
|
20899
|
-
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
20900
|
-
blocks.push({ type: "hr" });
|
|
20901
|
-
i++;
|
|
20902
|
-
continue;
|
|
20903
|
-
}
|
|
20904
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
20905
|
-
if (headingMatch) {
|
|
20906
|
-
blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
|
|
20907
|
-
i++;
|
|
20908
|
-
continue;
|
|
20909
|
-
}
|
|
20910
|
-
if (/^<table[\s>]/i.test(line.trimStart())) {
|
|
20911
|
-
const htmlLines = [];
|
|
20972
|
+
if (line.trim().startsWith("<table>")) {
|
|
20973
|
+
const collected2 = [];
|
|
20912
20974
|
let depth = 0;
|
|
20913
20975
|
while (i < lines.length) {
|
|
20914
20976
|
const l = lines[i];
|
|
20915
|
-
|
|
20916
|
-
depth += (l.match(/<table
|
|
20917
|
-
depth -= (l.match(/<\/table>/
|
|
20977
|
+
collected2.push(l);
|
|
20978
|
+
depth += (l.match(/<table>/g) || []).length;
|
|
20979
|
+
depth -= (l.match(/<\/table>/g) || []).length;
|
|
20918
20980
|
i++;
|
|
20919
20981
|
if (depth <= 0) break;
|
|
20920
20982
|
}
|
|
20921
|
-
|
|
20983
|
+
units.push({ kind: "html-table", raw: collected2.join("\n"), lines: collected2 });
|
|
20922
20984
|
continue;
|
|
20923
20985
|
}
|
|
20924
20986
|
if (line.trimStart().startsWith("|")) {
|
|
20925
|
-
const
|
|
20987
|
+
const collected2 = [];
|
|
20926
20988
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
20927
|
-
|
|
20928
|
-
if (/^[\s|:\-]+$/.test(row)) {
|
|
20929
|
-
i++;
|
|
20930
|
-
continue;
|
|
20931
|
-
}
|
|
20932
|
-
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
20933
|
-
if (cells.length > 0) tableRows.push(cells);
|
|
20989
|
+
collected2.push(lines[i]);
|
|
20934
20990
|
i++;
|
|
20935
20991
|
}
|
|
20936
|
-
|
|
20992
|
+
units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
|
|
20937
20993
|
continue;
|
|
20938
20994
|
}
|
|
20939
|
-
if (line.
|
|
20940
|
-
|
|
20941
|
-
|
|
20942
|
-
quoteLines.push(lines[i].replace(/^>\s?/, ""));
|
|
20943
|
-
i++;
|
|
20944
|
-
}
|
|
20945
|
-
for (const ql of quoteLines) {
|
|
20946
|
-
blocks.push({ type: "blockquote", text: ql.trim() || "" });
|
|
20947
|
-
}
|
|
20995
|
+
if (/^-{3,}\s*$/.test(line.trim())) {
|
|
20996
|
+
units.push({ kind: "separator", raw: line.trim(), lines: [line.trim()] });
|
|
20997
|
+
i++;
|
|
20948
20998
|
continue;
|
|
20949
20999
|
}
|
|
20950
|
-
|
|
20951
|
-
|
|
20952
|
-
const indent = Math.floor(listMatch[1].length / 2);
|
|
20953
|
-
const ordered = /\d/.test(listMatch[2]);
|
|
20954
|
-
blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
|
|
21000
|
+
if (/^!\[image\]\([^)]*\)\s*$/.test(line.trim())) {
|
|
21001
|
+
units.push({ kind: "image", raw: line.trim(), lines: [line.trim()] });
|
|
20955
21002
|
i++;
|
|
20956
21003
|
continue;
|
|
20957
21004
|
}
|
|
20958
|
-
|
|
20959
|
-
i
|
|
21005
|
+
const collected = [];
|
|
21006
|
+
while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("|") && !lines[i].trim().startsWith("<table>")) {
|
|
21007
|
+
collected.push(lines[i].trim());
|
|
21008
|
+
i++;
|
|
21009
|
+
}
|
|
21010
|
+
units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
|
|
20960
21011
|
}
|
|
20961
|
-
return
|
|
21012
|
+
return units;
|
|
20962
21013
|
}
|
|
20963
|
-
function
|
|
20964
|
-
|
|
20965
|
-
|
|
20966
|
-
|
|
20967
|
-
|
|
20968
|
-
|
|
20969
|
-
|
|
20970
|
-
|
|
20971
|
-
const idx = match.index;
|
|
20972
|
-
if (idx > lastIdx) {
|
|
20973
|
-
spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
|
|
21014
|
+
function alignUnits(a, b) {
|
|
21015
|
+
const m = a.length, n = b.length;
|
|
21016
|
+
if (m * n > 4e6) {
|
|
21017
|
+
const result2 = [];
|
|
21018
|
+
let pre = 0;
|
|
21019
|
+
while (pre < m && pre < n && a[pre] === b[pre]) {
|
|
21020
|
+
result2.push([pre, pre]);
|
|
21021
|
+
pre++;
|
|
20974
21022
|
}
|
|
20975
|
-
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
} else if (raw.startsWith("**") || raw.startsWith("__")) {
|
|
20981
|
-
spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
|
|
21023
|
+
let suf = 0;
|
|
21024
|
+
while (suf < m - pre && suf < n - pre && a[m - 1 - suf] === b[n - 1 - suf]) suf++;
|
|
21025
|
+
const aMid = m - pre - suf, bMid = n - pre - suf;
|
|
21026
|
+
if (aMid === bMid) {
|
|
21027
|
+
for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, pre + i2]);
|
|
20982
21028
|
} else {
|
|
20983
|
-
|
|
21029
|
+
for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, null]);
|
|
21030
|
+
for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
|
|
20984
21031
|
}
|
|
20985
|
-
|
|
21032
|
+
for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
|
|
21033
|
+
return result2;
|
|
20986
21034
|
}
|
|
20987
|
-
|
|
20988
|
-
|
|
21035
|
+
const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
|
|
21036
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
21037
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
21038
|
+
dp[i2][j2] = a[i2 - 1] === b[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
21039
|
+
}
|
|
20989
21040
|
}
|
|
20990
|
-
|
|
20991
|
-
|
|
21041
|
+
const matches = [];
|
|
21042
|
+
let i = m, j = n;
|
|
21043
|
+
while (i > 0 && j > 0) {
|
|
21044
|
+
if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
21045
|
+
matches.push([i - 1, j - 1]);
|
|
21046
|
+
i--;
|
|
21047
|
+
j--;
|
|
21048
|
+
} else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
|
|
21049
|
+
else j--;
|
|
20992
21050
|
}
|
|
20993
|
-
|
|
20994
|
-
|
|
20995
|
-
|
|
20996
|
-
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
21001
|
-
|
|
21002
|
-
|
|
21003
|
-
|
|
21004
|
-
|
|
21005
|
-
|
|
21006
|
-
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
21051
|
+
matches.reverse();
|
|
21052
|
+
const result = [];
|
|
21053
|
+
let ai = 0, bi = 0;
|
|
21054
|
+
const flushGap = (aEnd, bEnd) => {
|
|
21055
|
+
if (aEnd - ai === bEnd - bi) {
|
|
21056
|
+
while (ai < aEnd) result.push([ai++, bi++]);
|
|
21057
|
+
return;
|
|
21058
|
+
}
|
|
21059
|
+
while (ai < aEnd && bi < bEnd) {
|
|
21060
|
+
const sim = normalizedSimilarity(a[ai], b[bi]);
|
|
21061
|
+
if (sim >= 0.4) {
|
|
21062
|
+
if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
|
|
21063
|
+
result.push([ai++, null]);
|
|
21064
|
+
} else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
|
|
21065
|
+
result.push([null, bi++]);
|
|
21066
|
+
} else {
|
|
21067
|
+
result.push([ai++, bi++]);
|
|
21068
|
+
}
|
|
21069
|
+
} else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
|
|
21070
|
+
else result.push([null, bi++]);
|
|
21071
|
+
}
|
|
21072
|
+
while (ai < aEnd) result.push([ai++, null]);
|
|
21073
|
+
while (bi < bEnd) result.push([null, bi++]);
|
|
21074
|
+
};
|
|
21075
|
+
for (const [pi, pj] of matches) {
|
|
21076
|
+
flushGap(pi, pj);
|
|
21077
|
+
result.push([ai++, bi++]);
|
|
21016
21078
|
}
|
|
21017
|
-
|
|
21018
|
-
return
|
|
21019
|
-
}
|
|
21020
|
-
function headingParaPrId(level) {
|
|
21021
|
-
if (level === 1) return PARA_H1;
|
|
21022
|
-
if (level === 2) return PARA_H2;
|
|
21023
|
-
if (level === 3) return PARA_H3;
|
|
21024
|
-
return PARA_H4;
|
|
21079
|
+
flushGap(m, n);
|
|
21080
|
+
return result;
|
|
21025
21081
|
}
|
|
21026
|
-
function
|
|
21027
|
-
|
|
21028
|
-
|
|
21029
|
-
|
|
21030
|
-
|
|
21082
|
+
function bestSimInRange(arr, from, to, target) {
|
|
21083
|
+
let best = 0;
|
|
21084
|
+
for (let k = from; k <= to && k < arr.length; k++) {
|
|
21085
|
+
const s = normalizedSimilarity(arr[k], target);
|
|
21086
|
+
if (s > best) best = s;
|
|
21087
|
+
}
|
|
21088
|
+
return best;
|
|
21031
21089
|
}
|
|
21032
|
-
function
|
|
21033
|
-
return
|
|
21034
|
-
<ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
|
|
21035
|
-
<ocf:rootfiles>
|
|
21036
|
-
<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
|
|
21037
|
-
</ocf:rootfiles>
|
|
21038
|
-
</ocf:container>`;
|
|
21090
|
+
function escapeGfm(text) {
|
|
21091
|
+
return text.replace(/~/g, "\\~");
|
|
21039
21092
|
}
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
21045
|
-
|
|
21046
|
-
|
|
21047
|
-
|
|
21048
|
-
|
|
21049
|
-
|
|
21050
|
-
|
|
21051
|
-
</opf:package>`;
|
|
21093
|
+
var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
|
|
21094
|
+
function sanitizeText(text) {
|
|
21095
|
+
let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
|
|
21096
|
+
if (result.length <= 30 && result.includes(" ")) {
|
|
21097
|
+
const tokens = result.split(" ");
|
|
21098
|
+
const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-ㄱ-ㆎ]/.test(t)).length;
|
|
21099
|
+
if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
|
|
21100
|
+
result = tokens.join("");
|
|
21101
|
+
}
|
|
21102
|
+
}
|
|
21103
|
+
return result;
|
|
21052
21104
|
}
|
|
21053
|
-
function
|
|
21054
|
-
|
|
21055
|
-
const italicAttr = italic ? ` italic="1"` : "";
|
|
21056
|
-
const effFont = bold ? 2 : fontId;
|
|
21057
|
-
return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
|
|
21058
|
-
<hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
|
|
21059
|
-
<hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
|
|
21060
|
-
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
21061
|
-
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
21062
|
-
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
21063
|
-
</hh:charPr>`;
|
|
21105
|
+
function normForMatch(text) {
|
|
21106
|
+
return sanitizeText(text).replace(/\s+/g, " ").trim();
|
|
21064
21107
|
}
|
|
21065
|
-
function
|
|
21066
|
-
|
|
21067
|
-
const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
|
|
21068
|
-
const snapGrid = keepWord ? "0" : "1";
|
|
21069
|
-
return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
|
|
21070
|
-
<hh:align horizontal="${align}" vertical="BASELINE"/>
|
|
21071
|
-
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
21072
|
-
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
21073
|
-
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
21074
|
-
<hh:margin><hc:intent value="${indent}" unit="HWPUNIT"/><hc:left value="${left}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="${spaceBefore}" unit="HWPUNIT"/><hc:next value="${spaceAfter}" unit="HWPUNIT"/></hh:margin>
|
|
21075
|
-
<hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
|
|
21076
|
-
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
21077
|
-
</hh:paraPr>`;
|
|
21108
|
+
function unescapeGfm(text) {
|
|
21109
|
+
return text.replace(/\\~/g, "~");
|
|
21078
21110
|
}
|
|
21079
|
-
|
|
21080
|
-
|
|
21081
|
-
|
|
21082
|
-
var CHAR_VARIANT_BASE = 11;
|
|
21083
|
-
var GONGMUN_BODY_RATIO = 95;
|
|
21084
|
-
function plainRenderText(text) {
|
|
21085
|
-
return parseInlineMarkdown(text).map((s) => s.text).join("");
|
|
21111
|
+
function summarize(text) {
|
|
21112
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
21113
|
+
return t.length > 80 ? t.slice(0, 77) + "..." : t;
|
|
21086
21114
|
}
|
|
21087
|
-
function
|
|
21088
|
-
const
|
|
21089
|
-
if (
|
|
21090
|
-
|
|
21091
|
-
|
|
21092
|
-
const
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
let
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
|
|
21100
|
-
|
|
21101
|
-
|
|
21102
|
-
|
|
21103
|
-
|
|
21104
|
-
|
|
21105
|
-
|
|
21106
|
-
|
|
21107
|
-
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
|
|
21115
|
+
function replicateGfmTable(table) {
|
|
21116
|
+
const { cells, rows: numRows, cols: numCols } = table;
|
|
21117
|
+
if (numRows === 0 || numCols === 0) return null;
|
|
21118
|
+
if (numRows === 1 && numCols === 1) return null;
|
|
21119
|
+
if (numCols === 1) return null;
|
|
21120
|
+
const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
|
|
21121
|
+
const skip = /* @__PURE__ */ new Set();
|
|
21122
|
+
for (let r = 0; r < numRows; r++) {
|
|
21123
|
+
for (let c = 0; c < numCols; c++) {
|
|
21124
|
+
if (skip.has(`${r},${c}`)) continue;
|
|
21125
|
+
const cell = cells[r]?.[c];
|
|
21126
|
+
if (!cell) continue;
|
|
21127
|
+
display[r][c] = {
|
|
21128
|
+
text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
|
|
21129
|
+
gridR: r,
|
|
21130
|
+
gridC: c
|
|
21131
|
+
};
|
|
21132
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
21133
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
21134
|
+
if (dr === 0 && dc === 0) continue;
|
|
21135
|
+
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
21136
|
+
}
|
|
21137
|
+
}
|
|
21138
|
+
c += cell.colSpan - 1;
|
|
21139
|
+
}
|
|
21140
|
+
}
|
|
21141
|
+
const uniqueRows = [];
|
|
21142
|
+
let pendingLabelRow = null;
|
|
21143
|
+
for (let r = 0; r < display.length; r++) {
|
|
21144
|
+
const row = display[r];
|
|
21145
|
+
if (row.every((cell) => cell.text === "")) continue;
|
|
21146
|
+
const nonEmptyCols = row.filter((cell) => cell.text !== "");
|
|
21147
|
+
const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
|
|
21148
|
+
if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
|
|
21149
|
+
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
21150
|
+
pendingLabelRow = row;
|
|
21111
21151
|
continue;
|
|
21112
21152
|
}
|
|
21113
|
-
if (
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21153
|
+
if (pendingLabelRow) {
|
|
21154
|
+
if (row[0].text === "") row[0] = pendingLabelRow[0];
|
|
21155
|
+
else uniqueRows.push(pendingLabelRow);
|
|
21156
|
+
pendingLabelRow = null;
|
|
21157
|
+
}
|
|
21158
|
+
uniqueRows.push(row);
|
|
21118
21159
|
}
|
|
21119
|
-
|
|
21160
|
+
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
21161
|
+
return uniqueRows.length > 0 ? uniqueRows : null;
|
|
21120
21162
|
}
|
|
21121
|
-
function
|
|
21122
|
-
const
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21163
|
+
function parseGfmTable(lines) {
|
|
21164
|
+
const rows = [];
|
|
21165
|
+
for (const line of lines) {
|
|
21166
|
+
const trimmed = line.trim();
|
|
21167
|
+
if (!trimmed.startsWith("|")) continue;
|
|
21168
|
+
const cells = trimmed.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim());
|
|
21169
|
+
if (cells.length === 0) continue;
|
|
21170
|
+
if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
|
21171
|
+
rows.push(cells);
|
|
21172
|
+
}
|
|
21173
|
+
return rows;
|
|
21126
21174
|
}
|
|
21127
|
-
function
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21175
|
+
function unescapeGfmCell(text) {
|
|
21176
|
+
return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\~/g, "~");
|
|
21177
|
+
}
|
|
21178
|
+
function replicateCellInnerHtml(cell) {
|
|
21179
|
+
if (cell.blocks?.length) {
|
|
21180
|
+
return cell.blocks.map((b) => {
|
|
21181
|
+
if (b.type === "table" && b.table) {
|
|
21182
|
+
const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
|
|
21183
|
+
return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
|
|
21184
|
+
}
|
|
21185
|
+
if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
|
|
21186
|
+
const t = sanitizeText(b.text ?? "");
|
|
21187
|
+
return t ? t.replace(/\n/g, "<br>") : "";
|
|
21188
|
+
}).filter(Boolean).join("<br>");
|
|
21136
21189
|
}
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
21151
|
-
|
|
21152
|
-
|
|
21153
|
-
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21157
|
-
|
|
21190
|
+
return sanitizeText(cell.text).replace(/\n/g, "<br>");
|
|
21191
|
+
}
|
|
21192
|
+
function replicateTableToHtml(table) {
|
|
21193
|
+
const rows = replicateHtmlTable(table);
|
|
21194
|
+
const lines = ["<table>"];
|
|
21195
|
+
for (let r = 0; r < rows.length; r++) {
|
|
21196
|
+
const tag = rows[r].tag;
|
|
21197
|
+
const rowHtml = rows[r].cells.map((cell) => {
|
|
21198
|
+
const attrs = [];
|
|
21199
|
+
if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
|
|
21200
|
+
if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
|
|
21201
|
+
const attrStr = attrs.length ? " " + attrs.join(" ") : "";
|
|
21202
|
+
return `<${tag}${attrStr}>${cell.inner}</${tag}>`;
|
|
21203
|
+
});
|
|
21204
|
+
if (rowHtml.length) lines.push(`<tr>${rowHtml.join("")}</tr>`);
|
|
21205
|
+
}
|
|
21206
|
+
lines.push("</table>");
|
|
21207
|
+
return lines.join("\n");
|
|
21208
|
+
}
|
|
21209
|
+
function replicateHtmlTable(table) {
|
|
21210
|
+
const { cells, rows: numRows, cols: numCols } = table;
|
|
21211
|
+
const skip = /* @__PURE__ */ new Set();
|
|
21212
|
+
const result = [];
|
|
21213
|
+
for (let r = 0; r < numRows; r++) {
|
|
21214
|
+
const tag = r === 0 ? "th" : "td";
|
|
21215
|
+
const rowCells = [];
|
|
21216
|
+
for (let c = 0; c < numCols; c++) {
|
|
21217
|
+
if (skip.has(`${r},${c}`)) continue;
|
|
21218
|
+
const cell = cells[r]?.[c];
|
|
21219
|
+
if (!cell) continue;
|
|
21220
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
21221
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
21222
|
+
if (dr === 0 && dc === 0) continue;
|
|
21223
|
+
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
21224
|
+
}
|
|
21225
|
+
}
|
|
21226
|
+
rowCells.push({
|
|
21227
|
+
inner: replicateCellInnerHtml(cell),
|
|
21228
|
+
colSpan: cell.colSpan,
|
|
21229
|
+
rowSpan: cell.rowSpan,
|
|
21230
|
+
gridR: r,
|
|
21231
|
+
gridC: c
|
|
21232
|
+
});
|
|
21233
|
+
}
|
|
21234
|
+
if (rowCells.length) result.push({ tag, cells: rowCells });
|
|
21235
|
+
}
|
|
21236
|
+
return result;
|
|
21237
|
+
}
|
|
21238
|
+
function parseHtmlTable(raw) {
|
|
21239
|
+
const re = /<(\/?)(table|tr|td|th)((?:"[^"]*"|'[^']*'|[^>"'])*?)>/gi;
|
|
21240
|
+
let depth = 0;
|
|
21241
|
+
let currentRow = null;
|
|
21242
|
+
let cellStart = -1;
|
|
21243
|
+
let cellInfo = null;
|
|
21244
|
+
const rows = [];
|
|
21245
|
+
let m;
|
|
21246
|
+
while ((m = re.exec(raw)) !== null) {
|
|
21247
|
+
const isClose = m[1] === "/";
|
|
21248
|
+
const tag = m[2].toLowerCase();
|
|
21249
|
+
const attrs = m[3] || "";
|
|
21250
|
+
if (tag === "table") {
|
|
21251
|
+
depth += isClose ? -1 : 1;
|
|
21252
|
+
if (depth < 0) return null;
|
|
21253
|
+
continue;
|
|
21254
|
+
}
|
|
21255
|
+
if (depth !== 1) continue;
|
|
21256
|
+
if (tag === "tr") {
|
|
21257
|
+
if (!isClose) currentRow = [];
|
|
21258
|
+
else if (currentRow) {
|
|
21259
|
+
rows.push({ tag: rows.length === 0 ? "th" : "td", cells: currentRow });
|
|
21260
|
+
currentRow = null;
|
|
21261
|
+
}
|
|
21262
|
+
} else {
|
|
21263
|
+
if (!isClose) {
|
|
21264
|
+
const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
21265
|
+
const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
21266
|
+
cellStart = m.index + m[0].length;
|
|
21267
|
+
cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
|
|
21268
|
+
} else if (cellStart >= 0 && cellInfo && currentRow) {
|
|
21269
|
+
currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
|
|
21270
|
+
cellStart = -1;
|
|
21271
|
+
cellInfo = null;
|
|
21272
|
+
}
|
|
21273
|
+
}
|
|
21158
21274
|
}
|
|
21159
|
-
return
|
|
21160
|
-
|
|
21161
|
-
</hh:charProperties>`;
|
|
21275
|
+
if (depth !== 0) return null;
|
|
21276
|
+
return rows;
|
|
21162
21277
|
}
|
|
21163
|
-
|
|
21164
|
-
|
|
21165
|
-
|
|
21166
|
-
|
|
21167
|
-
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
|
|
21171
|
-
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
|
|
21172
|
-
paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
|
|
21173
|
-
paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
|
|
21174
|
-
];
|
|
21175
|
-
return `<hh:paraProperties itemCnt="${base2.length}">
|
|
21176
|
-
${base2.join("\n")}
|
|
21177
|
-
</hh:paraProperties>`;
|
|
21278
|
+
var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-9a-zA-Z가-힣]{1,6}[)\]][.:]?|[ⅰ-ⅹⅠ-Ⅹ①-⑮][.)\]:]?)$/u;
|
|
21279
|
+
function htmlCellInnerToLines(inner) {
|
|
21280
|
+
let hadNonText = false;
|
|
21281
|
+
let work = inner;
|
|
21282
|
+
if (/<table[\s>]/i.test(work)) {
|
|
21283
|
+
hadNonText = true;
|
|
21284
|
+
work = removeNestedTables(work);
|
|
21178
21285
|
}
|
|
21179
|
-
|
|
21180
|
-
|
|
21181
|
-
|
|
21182
|
-
paraPr(0, { lineSpacing: ls, keepWord: true }),
|
|
21183
|
-
paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
|
|
21184
|
-
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
|
|
21185
|
-
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
21186
|
-
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
21187
|
-
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
|
|
21188
|
-
paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
|
|
21189
|
-
paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
|
|
21190
|
-
];
|
|
21191
|
-
for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
|
|
21192
|
-
const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
|
|
21193
|
-
const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
|
|
21194
|
-
base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
|
|
21286
|
+
if (/<img\s/i.test(work)) {
|
|
21287
|
+
hadNonText = true;
|
|
21288
|
+
work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
|
|
21195
21289
|
}
|
|
21196
|
-
|
|
21197
|
-
return
|
|
21198
|
-
${base.join("\n")}
|
|
21199
|
-
</hh:paraProperties>`;
|
|
21290
|
+
const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
21291
|
+
return { lines, hadNonText };
|
|
21200
21292
|
}
|
|
21201
|
-
function
|
|
21202
|
-
const
|
|
21203
|
-
|
|
21204
|
-
|
|
21205
|
-
|
|
21206
|
-
|
|
21207
|
-
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21216
|
-
|
|
21217
|
-
|
|
21218
|
-
|
|
21219
|
-
|
|
21220
|
-
|
|
21221
|
-
<hh:fontface lang="LATIN" fontCnt="3">
|
|
21222
|
-
<hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
|
|
21223
|
-
<hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
|
|
21224
|
-
</hh:font>
|
|
21225
|
-
<hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
|
|
21226
|
-
<hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
21227
|
-
</hh:font>
|
|
21228
|
-
<hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
|
|
21229
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
21230
|
-
</hh:font>
|
|
21231
|
-
</hh:fontface>
|
|
21232
|
-
<hh:fontface lang="HANJA" fontCnt="1">
|
|
21233
|
-
<hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
|
|
21234
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21235
|
-
</hh:font>
|
|
21236
|
-
</hh:fontface>
|
|
21237
|
-
<hh:fontface lang="JAPANESE" fontCnt="1">
|
|
21238
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21239
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21240
|
-
</hh:font>
|
|
21241
|
-
</hh:fontface>
|
|
21242
|
-
<hh:fontface lang="OTHER" fontCnt="1">
|
|
21243
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21244
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21245
|
-
</hh:font>
|
|
21246
|
-
</hh:fontface>
|
|
21247
|
-
<hh:fontface lang="SYMBOL" fontCnt="1">
|
|
21248
|
-
<hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
|
|
21249
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21250
|
-
</hh:font>
|
|
21251
|
-
</hh:fontface>
|
|
21252
|
-
<hh:fontface lang="USER" fontCnt="1">
|
|
21253
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21254
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21255
|
-
</hh:font>
|
|
21256
|
-
</hh:fontface>
|
|
21257
|
-
</hh:fontfaces>
|
|
21258
|
-
<hh:borderFills itemCnt="2">
|
|
21259
|
-
<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21260
|
-
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21261
|
-
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21262
|
-
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21263
|
-
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21264
|
-
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21265
|
-
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21266
|
-
</hh:borderFill>
|
|
21267
|
-
<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21268
|
-
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21269
|
-
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21270
|
-
<hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21271
|
-
<hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21272
|
-
<hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21273
|
-
<hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21274
|
-
</hh:borderFill>
|
|
21275
|
-
</hh:borderFills>
|
|
21276
|
-
${charPropsXml}
|
|
21277
|
-
<hh:tabProperties itemCnt="0"/>
|
|
21278
|
-
<hh:numberings itemCnt="0"/>
|
|
21279
|
-
<hh:bullets itemCnt="0"/>
|
|
21280
|
-
${paraPropsXml}
|
|
21281
|
-
<hh:styles itemCnt="1">
|
|
21282
|
-
<hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
|
|
21283
|
-
</hh:styles>
|
|
21284
|
-
</hh:refList>
|
|
21285
|
-
<hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
|
|
21286
|
-
</hh:head>`;
|
|
21293
|
+
function extractTopLevelTables(html) {
|
|
21294
|
+
const result = [];
|
|
21295
|
+
let depth = 0;
|
|
21296
|
+
let start = -1;
|
|
21297
|
+
const re = /<(\/?)table(?:[\s>]|>)/gi;
|
|
21298
|
+
let m;
|
|
21299
|
+
while ((m = re.exec(html)) !== null) {
|
|
21300
|
+
if (m[1] !== "/") {
|
|
21301
|
+
if (depth === 0) start = m.index;
|
|
21302
|
+
depth++;
|
|
21303
|
+
} else {
|
|
21304
|
+
depth--;
|
|
21305
|
+
if (depth === 0 && start >= 0) {
|
|
21306
|
+
result.push(html.slice(start, m.index + m[0].length));
|
|
21307
|
+
start = -1;
|
|
21308
|
+
}
|
|
21309
|
+
if (depth < 0) depth = 0;
|
|
21310
|
+
}
|
|
21311
|
+
}
|
|
21312
|
+
return result;
|
|
21287
21313
|
}
|
|
21288
|
-
function
|
|
21289
|
-
|
|
21290
|
-
|
|
21291
|
-
|
|
21292
|
-
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21314
|
+
function removeNestedTables(html) {
|
|
21315
|
+
let result = "";
|
|
21316
|
+
let depth = 0;
|
|
21317
|
+
const re = /<(\/?)table(?:[\s>]|>)/gi;
|
|
21318
|
+
let last = 0;
|
|
21319
|
+
let m;
|
|
21320
|
+
while ((m = re.exec(html)) !== null) {
|
|
21321
|
+
if (m[1] !== "/") {
|
|
21322
|
+
if (depth === 0) result += html.slice(last, m.index);
|
|
21323
|
+
depth++;
|
|
21324
|
+
} else {
|
|
21325
|
+
depth--;
|
|
21326
|
+
if (depth === 0) last = m.index + m[0].length;
|
|
21327
|
+
if (depth < 0) depth = 0;
|
|
21328
|
+
}
|
|
21329
|
+
}
|
|
21330
|
+
if (depth === 0) result += html.slice(last);
|
|
21331
|
+
return result;
|
|
21298
21332
|
}
|
|
21333
|
+
|
|
21334
|
+
// src/hwpx/gen-table.ts
|
|
21299
21335
|
var TABLE_ID_BASE = 1e3;
|
|
21300
21336
|
var tableIdCounter = TABLE_ID_BASE;
|
|
21301
21337
|
function nextTableId() {
|
|
@@ -21385,41 +21421,18 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3) {
|
|
|
21385
21421
|
}
|
|
21386
21422
|
return `<hp:tbl id="${tblId}" zOrder="0" numberingType="TABLE" pageBreak="CELL" repeatHeader="0" rowCnt="${rowCnt}" colCnt="${colCnt}" cellSpacing="0" borderFillIDRef="2" noShading="0"><hp:sz width="${tblW}" widthRelTo="ABSOLUTE" height="${cellH * rowCnt}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:inMargin left="510" right="510" top="141" bottom="141"/>` + trXmls.join("") + `</hp:tbl>`;
|
|
21387
21423
|
}
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
run.push(i);
|
|
21401
|
-
i++;
|
|
21402
|
-
continue;
|
|
21403
|
-
}
|
|
21404
|
-
if (t === "table" || t === "html_table") {
|
|
21405
|
-
let j = i + 1;
|
|
21406
|
-
while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
|
|
21407
|
-
if (j < blocks.length && blocks[j].type === "list_item") {
|
|
21408
|
-
i = j;
|
|
21409
|
-
continue;
|
|
21410
|
-
}
|
|
21411
|
-
}
|
|
21412
|
-
break;
|
|
21413
|
-
}
|
|
21414
|
-
const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
|
|
21415
|
-
const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
|
|
21416
|
-
const numberer = new GongmunNumberer(gongmun.numbering);
|
|
21417
|
-
run.forEach((bi, k) => {
|
|
21418
|
-
const marker = numberer.next(depths[k], suppress[k]);
|
|
21419
|
-
result.set(bi, { marker, depth: depths[k] });
|
|
21420
|
-
});
|
|
21421
|
-
}
|
|
21422
|
-
return result;
|
|
21424
|
+
|
|
21425
|
+
// src/hwpx/gen-section.ts
|
|
21426
|
+
function generateSecPr(gongmun) {
|
|
21427
|
+
const m = gongmun ? {
|
|
21428
|
+
top: mmToHwpunit(gongmun.margins.top),
|
|
21429
|
+
bottom: mmToHwpunit(gongmun.margins.bottom),
|
|
21430
|
+
left: mmToHwpunit(gongmun.margins.left),
|
|
21431
|
+
right: mmToHwpunit(gongmun.margins.right),
|
|
21432
|
+
header: 0,
|
|
21433
|
+
footer: 0
|
|
21434
|
+
} : { top: 8504, bottom: 4252, left: 5670, right: 4252, header: 2835, footer: 2835 };
|
|
21435
|
+
return `<hp:secPr textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="0" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:pagePr landscape="WIDELY" width="59528" height="84188" gutterType="LEFT_ONLY"><hp:margin header="${m.header}" footer="${m.footer}" gutter="0" left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr></hp:secPr>`;
|
|
21423
21436
|
}
|
|
21424
21437
|
function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null) {
|
|
21425
21438
|
const paraXmls = [];
|
|
@@ -21542,6 +21555,24 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
|
|
|
21542
21555
|
</hs:sec>`;
|
|
21543
21556
|
}
|
|
21544
21557
|
|
|
21558
|
+
// src/hwpx/generator.ts
|
|
21559
|
+
async function markdownToHwpx(markdown, options) {
|
|
21560
|
+
const theme = resolveTheme(options?.theme);
|
|
21561
|
+
const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
|
|
21562
|
+
const blocks = parseMarkdownToBlocks(markdown);
|
|
21563
|
+
const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
|
|
21564
|
+
const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
|
|
21565
|
+
const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
|
|
21566
|
+
const zip = new JSZip6();
|
|
21567
|
+
zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
|
|
21568
|
+
zip.file("META-INF/container.xml", generateContainerXml());
|
|
21569
|
+
zip.file("Contents/content.hpf", generateManifest());
|
|
21570
|
+
zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
|
|
21571
|
+
zip.file("Contents/section0.xml", sectionXml);
|
|
21572
|
+
zip.file("Preview/PrvText.txt", buildPrvText(blocks));
|
|
21573
|
+
return await zip.generateAsync({ type: "arraybuffer" });
|
|
21574
|
+
}
|
|
21575
|
+
|
|
21545
21576
|
// src/diff/compare.ts
|
|
21546
21577
|
var SIMILARITY_THRESHOLD = 0.4;
|
|
21547
21578
|
async function compare(bufferA, bufferB, options) {
|
|
@@ -21677,7 +21708,7 @@ function diffTableCells(a, b) {
|
|
|
21677
21708
|
}
|
|
21678
21709
|
|
|
21679
21710
|
// src/roundtrip/patcher.ts
|
|
21680
|
-
import
|
|
21711
|
+
import JSZip7 from "jszip";
|
|
21681
21712
|
|
|
21682
21713
|
// src/roundtrip/table-rows.ts
|
|
21683
21714
|
var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
|
|
@@ -22315,7 +22346,7 @@ async function patchHwpx(original, editedMarkdown, options) {
|
|
|
22315
22346
|
}
|
|
22316
22347
|
let zip;
|
|
22317
22348
|
try {
|
|
22318
|
-
zip = await
|
|
22349
|
+
zip = await JSZip7.loadAsync(original);
|
|
22319
22350
|
} catch {
|
|
22320
22351
|
return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
|
|
22321
22352
|
}
|
|
@@ -23682,10 +23713,10 @@ function stageParaPatch(scan, para, newPlain, skip) {
|
|
|
23682
23713
|
}
|
|
23683
23714
|
|
|
23684
23715
|
// src/roundtrip/session.ts
|
|
23685
|
-
import
|
|
23716
|
+
import JSZip8 from "jszip";
|
|
23686
23717
|
async function buildState(bytes) {
|
|
23687
23718
|
const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
|
|
23688
|
-
const zip = await
|
|
23719
|
+
const zip = await JSZip8.loadAsync(bytes);
|
|
23689
23720
|
const sectionPaths = await resolveSectionEntryNames(zip);
|
|
23690
23721
|
if (sectionPaths.length === 0) {
|
|
23691
23722
|
throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
@@ -24266,7 +24297,7 @@ async function parseHwp(buffer, options) {
|
|
|
24266
24297
|
async function parsePdf(buffer, options) {
|
|
24267
24298
|
let parsePdfDocument;
|
|
24268
24299
|
try {
|
|
24269
|
-
const mod = await import("./parser-
|
|
24300
|
+
const mod = await import("./parser-KNQDRLZQ.js");
|
|
24270
24301
|
parsePdfDocument = mod.parsePdfDocument;
|
|
24271
24302
|
} catch {
|
|
24272
24303
|
return {
|
|
@@ -24401,4 +24432,4 @@ export {
|
|
|
24401
24432
|
parseHwpml,
|
|
24402
24433
|
fillForm
|
|
24403
24434
|
};
|
|
24404
|
-
//# sourceMappingURL=chunk-
|
|
24435
|
+
//# sourceMappingURL=chunk-DP37KF2X.js.map
|