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
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sanitizeHref,
|
|
20
20
|
stripDtd,
|
|
21
21
|
toArrayBuffer
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-553VTUVP.js";
|
|
23
23
|
import {
|
|
24
24
|
parsePageRange
|
|
25
25
|
} from "./chunk-GE43BE46.js";
|
|
@@ -279,9 +279,7 @@ async function detectZipFormat(buffer) {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
// src/hwpx/parser.ts
|
|
282
|
-
import
|
|
283
|
-
import { inflateRawSync } from "zlib";
|
|
284
|
-
import { DOMParser } from "@xmldom/xmldom";
|
|
282
|
+
import JSZip3 from "jszip";
|
|
285
283
|
|
|
286
284
|
// src/hwpx/com-fallback.ts
|
|
287
285
|
import { execFileSync } from "child_process";
|
|
@@ -390,6 +388,245 @@ function comResultToParseResult(pages, pageCount, warnings) {
|
|
|
390
388
|
};
|
|
391
389
|
}
|
|
392
390
|
|
|
391
|
+
// src/hwpx/parser-shared.ts
|
|
392
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
393
|
+
var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
|
|
394
|
+
var MAX_ZIP_ENTRIES = 500;
|
|
395
|
+
function clampSpan(val, max) {
|
|
396
|
+
return Math.max(1, Math.min(val, max));
|
|
397
|
+
}
|
|
398
|
+
var MAX_XML_DEPTH = 200;
|
|
399
|
+
function createSectionShared() {
|
|
400
|
+
return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
|
|
401
|
+
}
|
|
402
|
+
function createXmlParser(warnings) {
|
|
403
|
+
return new DOMParser({
|
|
404
|
+
onError(level, msg2) {
|
|
405
|
+
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
|
|
406
|
+
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function applyPageText(blocks, shared) {
|
|
411
|
+
const { headers, footers } = shared.pageText;
|
|
412
|
+
if (headers.length > 0) {
|
|
413
|
+
blocks.unshift(...headers.map((t) => ({ type: "paragraph", text: t, pageNumber: 1 })));
|
|
414
|
+
}
|
|
415
|
+
if (footers.length > 0) {
|
|
416
|
+
blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function findChildByLocalName(parent, name) {
|
|
420
|
+
const children = parent.childNodes;
|
|
421
|
+
if (!children) return null;
|
|
422
|
+
for (let i = 0; i < children.length; i++) {
|
|
423
|
+
const ch = children[i];
|
|
424
|
+
if (ch.nodeType !== 1) continue;
|
|
425
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
426
|
+
if (tag === name) return ch;
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function extractTextFromNode(node) {
|
|
431
|
+
let result = "";
|
|
432
|
+
const children = node.childNodes;
|
|
433
|
+
if (!children) return result;
|
|
434
|
+
for (let i = 0; i < children.length; i++) {
|
|
435
|
+
const child = children[i];
|
|
436
|
+
if (child.nodeType === 3) result += child.textContent || "";
|
|
437
|
+
else if (child.nodeType === 1) result += extractTextFromNode(child);
|
|
438
|
+
}
|
|
439
|
+
return result.trim();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/hwpx/styles.ts
|
|
443
|
+
async function extractHwpxStyles(zip, decompressed) {
|
|
444
|
+
const result = {
|
|
445
|
+
charProperties: /* @__PURE__ */ new Map(),
|
|
446
|
+
styles: /* @__PURE__ */ new Map(),
|
|
447
|
+
numberings: /* @__PURE__ */ new Map(),
|
|
448
|
+
bullets: /* @__PURE__ */ new Map(),
|
|
449
|
+
paraHeadings: /* @__PURE__ */ new Map()
|
|
450
|
+
};
|
|
451
|
+
const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
|
|
452
|
+
for (const hp of headerPaths) {
|
|
453
|
+
const hpLower = hp.toLowerCase();
|
|
454
|
+
const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
|
|
455
|
+
if (!file) continue;
|
|
456
|
+
try {
|
|
457
|
+
const xml = await file.async("text");
|
|
458
|
+
if (decompressed) {
|
|
459
|
+
decompressed.total += xml.length * 2;
|
|
460
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
461
|
+
}
|
|
462
|
+
const parser = createXmlParser();
|
|
463
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
464
|
+
if (!doc.documentElement) continue;
|
|
465
|
+
parseCharProperties(doc, result.charProperties);
|
|
466
|
+
parseStyleElements(doc, result.styles);
|
|
467
|
+
const domDoc = doc;
|
|
468
|
+
parseNumberings(domDoc, result.numberings);
|
|
469
|
+
parseBullets(domDoc, result.bullets);
|
|
470
|
+
parseParaHeadings(domDoc, result.paraHeadings);
|
|
471
|
+
break;
|
|
472
|
+
} catch {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
function parseCharProperties(doc, map) {
|
|
479
|
+
const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
|
|
480
|
+
for (const tagName of tagNames) {
|
|
481
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
482
|
+
for (let i = 0; i < elements.length; i++) {
|
|
483
|
+
const el = elements[i];
|
|
484
|
+
const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
|
|
485
|
+
if (!id) continue;
|
|
486
|
+
const prop = {};
|
|
487
|
+
const height = el.getAttribute("height");
|
|
488
|
+
if (height) {
|
|
489
|
+
const parsedHeight = parseInt(height, 10);
|
|
490
|
+
if (!isNaN(parsedHeight) && parsedHeight > 0) {
|
|
491
|
+
prop.fontSize = parsedHeight / 100;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const bold = el.getAttribute("bold");
|
|
495
|
+
if (bold === "true" || bold === "1") prop.bold = true;
|
|
496
|
+
const italic = el.getAttribute("italic");
|
|
497
|
+
if (italic === "true" || italic === "1") prop.italic = true;
|
|
498
|
+
const fontFaces = el.getElementsByTagName("*");
|
|
499
|
+
for (let j = 0; j < fontFaces.length; j++) {
|
|
500
|
+
const ff = fontFaces[j];
|
|
501
|
+
const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
|
|
502
|
+
if (localTag === "fontface" || localTag === "fontRef") {
|
|
503
|
+
const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
|
|
504
|
+
if (face) {
|
|
505
|
+
prop.fontName = face;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
map.set(id, prop);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function parseStyleElements(doc, map) {
|
|
515
|
+
const tagNames = ["hh:style", "style", "hp:style"];
|
|
516
|
+
for (const tagName of tagNames) {
|
|
517
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
518
|
+
for (let i = 0; i < elements.length; i++) {
|
|
519
|
+
const el = elements[i];
|
|
520
|
+
const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
|
|
521
|
+
const name = el.getAttribute("name") || el.getAttribute("engName") || "";
|
|
522
|
+
const charPrId = el.getAttribute("charPrIDRef") || void 0;
|
|
523
|
+
const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
|
|
524
|
+
map.set(id, { name, charPrId, paraPrId });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function parseNumberings(doc, map) {
|
|
529
|
+
const tagNames = ["hh:numbering", "numbering"];
|
|
530
|
+
for (const tagName of tagNames) {
|
|
531
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
532
|
+
for (let i = 0; i < elements.length; i++) {
|
|
533
|
+
const el = elements[i];
|
|
534
|
+
const id = el.getAttribute("id") || "";
|
|
535
|
+
if (!id) continue;
|
|
536
|
+
const def = { heads: /* @__PURE__ */ new Map() };
|
|
537
|
+
const children = el.childNodes;
|
|
538
|
+
for (let j = 0; j < children.length; j++) {
|
|
539
|
+
const ch = children[j];
|
|
540
|
+
if (ch.nodeType !== 1) continue;
|
|
541
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
542
|
+
if (tag !== "paraHead") continue;
|
|
543
|
+
const level = parseInt(ch.getAttribute("level") || "", 10);
|
|
544
|
+
if (isNaN(level) || level < 1 || level > 10) continue;
|
|
545
|
+
const start = parseInt(ch.getAttribute("start") || "1", 10);
|
|
546
|
+
def.heads.set(level, {
|
|
547
|
+
numFormat: ch.getAttribute("numFormat") || "DIGIT",
|
|
548
|
+
text: ch.textContent || "",
|
|
549
|
+
start: isNaN(start) ? 1 : start
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (def.heads.size > 0) map.set(id, def);
|
|
553
|
+
}
|
|
554
|
+
if (map.size > 0) break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function parseBullets(doc, map) {
|
|
558
|
+
const tagNames = ["hh:bullet", "bullet"];
|
|
559
|
+
for (const tagName of tagNames) {
|
|
560
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
561
|
+
for (let i = 0; i < elements.length; i++) {
|
|
562
|
+
const el = elements[i];
|
|
563
|
+
const id = el.getAttribute("id") || "";
|
|
564
|
+
const char = el.getAttribute("char") || "";
|
|
565
|
+
if (id && char) map.set(id, char);
|
|
566
|
+
}
|
|
567
|
+
if (map.size > 0) break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function parseParaHeadings(doc, map) {
|
|
571
|
+
const tagNames = ["hh:paraPr", "paraPr"];
|
|
572
|
+
for (const tagName of tagNames) {
|
|
573
|
+
const elements = doc.getElementsByTagName(tagName);
|
|
574
|
+
for (let i = 0; i < elements.length; i++) {
|
|
575
|
+
const el = elements[i];
|
|
576
|
+
const id = el.getAttribute("id") || "";
|
|
577
|
+
if (!id) continue;
|
|
578
|
+
const heading = findChildByLocalName(el, "heading");
|
|
579
|
+
if (!heading) continue;
|
|
580
|
+
const type = heading.getAttribute("type") || "NONE";
|
|
581
|
+
if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
|
|
582
|
+
const level = parseInt(heading.getAttribute("level") || "0", 10);
|
|
583
|
+
map.set(id, {
|
|
584
|
+
type,
|
|
585
|
+
idRef: heading.getAttribute("idRef") || "0",
|
|
586
|
+
level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (map.size > 0) break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function detectHwpxHeadings(blocks, styleMap) {
|
|
593
|
+
if (blocks.some((b) => b.type === "heading")) return;
|
|
594
|
+
let baseFontSize = 0;
|
|
595
|
+
const sizeFreq = /* @__PURE__ */ new Map();
|
|
596
|
+
for (const b of blocks) {
|
|
597
|
+
if (b.style?.fontSize) {
|
|
598
|
+
sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
let maxCount = 0;
|
|
602
|
+
for (const [size, count] of sizeFreq) {
|
|
603
|
+
if (count > maxCount) {
|
|
604
|
+
maxCount = count;
|
|
605
|
+
baseFontSize = size;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
for (const block of blocks) {
|
|
609
|
+
if (block.type !== "paragraph" || !block.text) continue;
|
|
610
|
+
const text = block.text.trim();
|
|
611
|
+
if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
|
|
612
|
+
let level = 0;
|
|
613
|
+
if (baseFontSize > 0 && block.style?.fontSize) {
|
|
614
|
+
const ratio = block.style.fontSize / baseFontSize;
|
|
615
|
+
if (ratio >= HEADING_RATIO_H1) level = 1;
|
|
616
|
+
else if (ratio >= HEADING_RATIO_H2) level = 2;
|
|
617
|
+
else if (ratio >= HEADING_RATIO_H3) level = 3;
|
|
618
|
+
}
|
|
619
|
+
const compactText = text.replace(/\s+/g, "");
|
|
620
|
+
if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
|
|
621
|
+
if (level === 0) level = 3;
|
|
622
|
+
}
|
|
623
|
+
if (level > 0) {
|
|
624
|
+
block.type = "heading";
|
|
625
|
+
block.level = level;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
393
630
|
// src/hwpx/equation.ts
|
|
394
631
|
var CONVERT_MAP = {
|
|
395
632
|
TIMES: "\\times",
|
|
@@ -829,173 +1066,7 @@ function hmlToLatex(hmlEqStr) {
|
|
|
829
1066
|
return out;
|
|
830
1067
|
}
|
|
831
1068
|
|
|
832
|
-
// src/hwpx/
|
|
833
|
-
var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
|
|
834
|
-
var MAX_ZIP_ENTRIES = 500;
|
|
835
|
-
function clampSpan(val, max) {
|
|
836
|
-
return Math.max(1, Math.min(val, max));
|
|
837
|
-
}
|
|
838
|
-
var MAX_XML_DEPTH = 200;
|
|
839
|
-
function createSectionShared() {
|
|
840
|
-
return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
|
|
841
|
-
}
|
|
842
|
-
function createXmlParser(warnings) {
|
|
843
|
-
return new DOMParser({
|
|
844
|
-
onError(level, msg2) {
|
|
845
|
-
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
|
|
846
|
-
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async function extractHwpxStyles(zip, decompressed) {
|
|
851
|
-
const result = {
|
|
852
|
-
charProperties: /* @__PURE__ */ new Map(),
|
|
853
|
-
styles: /* @__PURE__ */ new Map(),
|
|
854
|
-
numberings: /* @__PURE__ */ new Map(),
|
|
855
|
-
bullets: /* @__PURE__ */ new Map(),
|
|
856
|
-
paraHeadings: /* @__PURE__ */ new Map()
|
|
857
|
-
};
|
|
858
|
-
const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
|
|
859
|
-
for (const hp of headerPaths) {
|
|
860
|
-
const hpLower = hp.toLowerCase();
|
|
861
|
-
const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
|
|
862
|
-
if (!file) continue;
|
|
863
|
-
try {
|
|
864
|
-
const xml = await file.async("text");
|
|
865
|
-
if (decompressed) {
|
|
866
|
-
decompressed.total += xml.length * 2;
|
|
867
|
-
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
868
|
-
}
|
|
869
|
-
const parser = createXmlParser();
|
|
870
|
-
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
871
|
-
if (!doc.documentElement) continue;
|
|
872
|
-
parseCharProperties(doc, result.charProperties);
|
|
873
|
-
parseStyleElements(doc, result.styles);
|
|
874
|
-
const domDoc = doc;
|
|
875
|
-
parseNumberings(domDoc, result.numberings);
|
|
876
|
-
parseBullets(domDoc, result.bullets);
|
|
877
|
-
parseParaHeadings(domDoc, result.paraHeadings);
|
|
878
|
-
break;
|
|
879
|
-
} catch {
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return result;
|
|
884
|
-
}
|
|
885
|
-
function parseCharProperties(doc, map) {
|
|
886
|
-
const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
|
|
887
|
-
for (const tagName of tagNames) {
|
|
888
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
889
|
-
for (let i = 0; i < elements.length; i++) {
|
|
890
|
-
const el = elements[i];
|
|
891
|
-
const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
|
|
892
|
-
if (!id) continue;
|
|
893
|
-
const prop = {};
|
|
894
|
-
const height = el.getAttribute("height");
|
|
895
|
-
if (height) {
|
|
896
|
-
const parsedHeight = parseInt(height, 10);
|
|
897
|
-
if (!isNaN(parsedHeight) && parsedHeight > 0) {
|
|
898
|
-
prop.fontSize = parsedHeight / 100;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const bold = el.getAttribute("bold");
|
|
902
|
-
if (bold === "true" || bold === "1") prop.bold = true;
|
|
903
|
-
const italic = el.getAttribute("italic");
|
|
904
|
-
if (italic === "true" || italic === "1") prop.italic = true;
|
|
905
|
-
const fontFaces = el.getElementsByTagName("*");
|
|
906
|
-
for (let j = 0; j < fontFaces.length; j++) {
|
|
907
|
-
const ff = fontFaces[j];
|
|
908
|
-
const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
|
|
909
|
-
if (localTag === "fontface" || localTag === "fontRef") {
|
|
910
|
-
const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
|
|
911
|
-
if (face) {
|
|
912
|
-
prop.fontName = face;
|
|
913
|
-
break;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
map.set(id, prop);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
function parseStyleElements(doc, map) {
|
|
922
|
-
const tagNames = ["hh:style", "style", "hp:style"];
|
|
923
|
-
for (const tagName of tagNames) {
|
|
924
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
925
|
-
for (let i = 0; i < elements.length; i++) {
|
|
926
|
-
const el = elements[i];
|
|
927
|
-
const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
|
|
928
|
-
const name = el.getAttribute("name") || el.getAttribute("engName") || "";
|
|
929
|
-
const charPrId = el.getAttribute("charPrIDRef") || void 0;
|
|
930
|
-
const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
|
|
931
|
-
map.set(id, { name, charPrId, paraPrId });
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
function parseNumberings(doc, map) {
|
|
936
|
-
const tagNames = ["hh:numbering", "numbering"];
|
|
937
|
-
for (const tagName of tagNames) {
|
|
938
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
939
|
-
for (let i = 0; i < elements.length; i++) {
|
|
940
|
-
const el = elements[i];
|
|
941
|
-
const id = el.getAttribute("id") || "";
|
|
942
|
-
if (!id) continue;
|
|
943
|
-
const def = { heads: /* @__PURE__ */ new Map() };
|
|
944
|
-
const children = el.childNodes;
|
|
945
|
-
for (let j = 0; j < children.length; j++) {
|
|
946
|
-
const ch = children[j];
|
|
947
|
-
if (ch.nodeType !== 1) continue;
|
|
948
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
949
|
-
if (tag !== "paraHead") continue;
|
|
950
|
-
const level = parseInt(ch.getAttribute("level") || "", 10);
|
|
951
|
-
if (isNaN(level) || level < 1 || level > 10) continue;
|
|
952
|
-
const start = parseInt(ch.getAttribute("start") || "1", 10);
|
|
953
|
-
def.heads.set(level, {
|
|
954
|
-
numFormat: ch.getAttribute("numFormat") || "DIGIT",
|
|
955
|
-
text: ch.textContent || "",
|
|
956
|
-
start: isNaN(start) ? 1 : start
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
if (def.heads.size > 0) map.set(id, def);
|
|
960
|
-
}
|
|
961
|
-
if (map.size > 0) break;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
function parseBullets(doc, map) {
|
|
965
|
-
const tagNames = ["hh:bullet", "bullet"];
|
|
966
|
-
for (const tagName of tagNames) {
|
|
967
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
968
|
-
for (let i = 0; i < elements.length; i++) {
|
|
969
|
-
const el = elements[i];
|
|
970
|
-
const id = el.getAttribute("id") || "";
|
|
971
|
-
const char = el.getAttribute("char") || "";
|
|
972
|
-
if (id && char) map.set(id, char);
|
|
973
|
-
}
|
|
974
|
-
if (map.size > 0) break;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
function parseParaHeadings(doc, map) {
|
|
978
|
-
const tagNames = ["hh:paraPr", "paraPr"];
|
|
979
|
-
for (const tagName of tagNames) {
|
|
980
|
-
const elements = doc.getElementsByTagName(tagName);
|
|
981
|
-
for (let i = 0; i < elements.length; i++) {
|
|
982
|
-
const el = elements[i];
|
|
983
|
-
const id = el.getAttribute("id") || "";
|
|
984
|
-
if (!id) continue;
|
|
985
|
-
const heading = findChildByLocalName(el, "heading");
|
|
986
|
-
if (!heading) continue;
|
|
987
|
-
const type = heading.getAttribute("type") || "NONE";
|
|
988
|
-
if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
|
|
989
|
-
const level = parseInt(heading.getAttribute("level") || "0", 10);
|
|
990
|
-
map.set(id, {
|
|
991
|
-
type,
|
|
992
|
-
idRef: heading.getAttribute("idRef") || "0",
|
|
993
|
-
level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
if (map.size > 0) break;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
1069
|
+
// src/hwpx/para-heading.ts
|
|
999
1070
|
var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
|
|
1000
1071
|
var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
|
|
1001
1072
|
function toRoman(n) {
|
|
@@ -1088,1025 +1159,973 @@ function resolveParaHeading(paraEl, ctx) {
|
|
|
1088
1159
|
});
|
|
1089
1160
|
return { prefix, headingLevel };
|
|
1090
1161
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
if (pages.some((p) => p && p.trim().length > 0)) {
|
|
1110
|
-
return comResultToParseResult(pages, pageCount, warnings2);
|
|
1162
|
+
|
|
1163
|
+
// src/hwpx/table-build.ts
|
|
1164
|
+
function buildTableWithCellMeta(state) {
|
|
1165
|
+
const table = buildTable(state.rows);
|
|
1166
|
+
if (state.caption) table.caption = state.caption;
|
|
1167
|
+
const anchors = [];
|
|
1168
|
+
{
|
|
1169
|
+
const covered = /* @__PURE__ */ new Set();
|
|
1170
|
+
for (let r = 0; r < table.rows; r++) {
|
|
1171
|
+
for (let c = 0; c < table.cols; c++) {
|
|
1172
|
+
if (covered.has(`${r},${c}`)) continue;
|
|
1173
|
+
const cell = table.cells[r]?.[c];
|
|
1174
|
+
if (!cell) continue;
|
|
1175
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1176
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
1177
|
+
if (dr === 0 && dc === 0) continue;
|
|
1178
|
+
if (r + dr < table.rows && c + dc < table.cols) covered.add(`${r + dr},${c + dc}`);
|
|
1179
|
+
}
|
|
1111
1180
|
}
|
|
1181
|
+
anchors.push(cell);
|
|
1182
|
+
c += cell.colSpan - 1;
|
|
1112
1183
|
}
|
|
1113
|
-
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.");
|
|
1114
1184
|
}
|
|
1115
1185
|
}
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (
|
|
1142
|
-
|
|
1186
|
+
const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
|
|
1187
|
+
const ordinalReliable = anchors.length === srcCount;
|
|
1188
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1189
|
+
let flatIdx = -1;
|
|
1190
|
+
for (const row of state.rows) {
|
|
1191
|
+
for (const src of row) {
|
|
1192
|
+
flatIdx++;
|
|
1193
|
+
const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
|
|
1194
|
+
if (!needsBlocks && !src.isHeader) continue;
|
|
1195
|
+
let target;
|
|
1196
|
+
const trimmed = src.text.trim();
|
|
1197
|
+
if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
|
|
1198
|
+
const cand = table.cells[src.rowAddr]?.[src.colAddr];
|
|
1199
|
+
if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
|
|
1200
|
+
}
|
|
1201
|
+
if (!target) {
|
|
1202
|
+
outer: for (const irRow of table.cells) {
|
|
1203
|
+
for (const cand of irRow) {
|
|
1204
|
+
if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
|
|
1205
|
+
target = cand;
|
|
1206
|
+
break outer;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (!target && ordinalReliable) {
|
|
1212
|
+
const cand = anchors[flatIdx];
|
|
1213
|
+
if (cand && !claimed.has(cand)) target = cand;
|
|
1214
|
+
}
|
|
1215
|
+
if (!target) continue;
|
|
1216
|
+
claimed.add(target);
|
|
1217
|
+
if (needsBlocks) target.blocks = src.blocks;
|
|
1218
|
+
if (src.isHeader) target.isHeader = true;
|
|
1143
1219
|
}
|
|
1144
1220
|
}
|
|
1145
|
-
|
|
1146
|
-
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
1147
|
-
detectHwpxHeadings(blocks, styleMap);
|
|
1148
|
-
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
1149
|
-
const markdown = blocksToMarkdown(blocks);
|
|
1150
|
-
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 };
|
|
1221
|
+
return table;
|
|
1151
1222
|
}
|
|
1152
|
-
function
|
|
1153
|
-
const
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1223
|
+
function completeTable(newTable, tableStack, blocks, ctx) {
|
|
1224
|
+
const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
|
|
1225
|
+
if (newTable.rows.length === 0) {
|
|
1226
|
+
if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
|
|
1227
|
+
return parentTable;
|
|
1156
1228
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1229
|
+
const ir = buildTableWithCellMeta(newTable);
|
|
1230
|
+
const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
|
|
1231
|
+
if (parentTable?.cell) {
|
|
1232
|
+
const cell = parentTable.cell;
|
|
1233
|
+
(cell.blocks ??= []).push(block);
|
|
1234
|
+
cell.hasStructure = true;
|
|
1235
|
+
let flat = convertTableToText(newTable.rows);
|
|
1236
|
+
if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
|
|
1237
|
+
if (flat) cell.text += (cell.text ? "\n" : "") + flat;
|
|
1238
|
+
} else {
|
|
1239
|
+
blocks.push(block);
|
|
1159
1240
|
}
|
|
1241
|
+
return parentTable;
|
|
1160
1242
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
return "image/wmf";
|
|
1177
|
-
case "emf":
|
|
1178
|
-
return "image/emf";
|
|
1179
|
-
case "svg":
|
|
1180
|
-
return "image/svg+xml";
|
|
1181
|
-
default:
|
|
1182
|
-
return "application/octet-stream";
|
|
1243
|
+
|
|
1244
|
+
// src/hwpx/section-walker.ts
|
|
1245
|
+
function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
|
|
1246
|
+
const parser = createXmlParser(warnings);
|
|
1247
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1248
|
+
if (!doc.documentElement) return [];
|
|
1249
|
+
const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
|
|
1250
|
+
ctx.shared.track.deleteDepth = 0;
|
|
1251
|
+
for (const tagName of ["hp:secPr", "secPr"]) {
|
|
1252
|
+
const els = doc.getElementsByTagName(tagName);
|
|
1253
|
+
if (els.length > 0) {
|
|
1254
|
+
const v = els[0].getAttribute("outlineShapeIDRef");
|
|
1255
|
+
if (v) ctx.outlineNumId = v;
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1183
1258
|
}
|
|
1259
|
+
const blocks = [];
|
|
1260
|
+
walkSection(doc.documentElement, blocks, null, [], ctx);
|
|
1261
|
+
return blocks;
|
|
1184
1262
|
}
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
|
|
1197
|
-
if (depth > MAX_XML_DEPTH) return;
|
|
1198
|
-
for (const block of blocks) {
|
|
1199
|
-
if (block.type === "image") {
|
|
1200
|
-
out.push({ block, ownerCell });
|
|
1201
|
-
} else if (block.type === "table" && block.table) {
|
|
1202
|
-
for (const row of block.table.cells) {
|
|
1203
|
-
for (const cell of row) {
|
|
1204
|
-
if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1263
|
+
function extractImageRef(el) {
|
|
1264
|
+
const children = el.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 === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
1271
|
+
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
1272
|
+
if (ref) return ref;
|
|
1207
1273
|
}
|
|
1274
|
+
const nested = extractImageRef(child);
|
|
1275
|
+
if (nested) return nested;
|
|
1208
1276
|
}
|
|
1277
|
+
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
1278
|
+
if (directRef) return directRef;
|
|
1279
|
+
return null;
|
|
1209
1280
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1281
|
+
function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1282
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
1283
|
+
const children = node.childNodes;
|
|
1284
|
+
if (!children) return;
|
|
1285
|
+
for (let i = 0; i < children.length; i++) {
|
|
1286
|
+
const el = children[i];
|
|
1287
|
+
if (el.nodeType !== 1) continue;
|
|
1288
|
+
const tag = el.tagName || el.localName || "";
|
|
1289
|
+
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1290
|
+
switch (localTag) {
|
|
1291
|
+
case "tbl": {
|
|
1292
|
+
if (tableCtx) tableStack.push(tableCtx);
|
|
1293
|
+
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1294
|
+
walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
|
|
1295
|
+
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
// 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
|
|
1299
|
+
case "caption":
|
|
1300
|
+
if (tableCtx) {
|
|
1301
|
+
const capText = collectSubListText(el, ctx);
|
|
1302
|
+
if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
|
|
1303
|
+
}
|
|
1304
|
+
break;
|
|
1305
|
+
case "tr":
|
|
1306
|
+
if (tableCtx) {
|
|
1307
|
+
tableCtx.currentRow = [];
|
|
1308
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1309
|
+
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
1310
|
+
tableCtx.currentRow = [];
|
|
1311
|
+
}
|
|
1312
|
+
break;
|
|
1313
|
+
case "tc":
|
|
1314
|
+
if (tableCtx) {
|
|
1315
|
+
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1316
|
+
if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
|
|
1317
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1318
|
+
if (tableCtx.cell) {
|
|
1319
|
+
tableCtx.currentRow.push(tableCtx.cell);
|
|
1320
|
+
tableCtx.cell = null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
break;
|
|
1324
|
+
case "cellAddr":
|
|
1325
|
+
if (tableCtx?.cell) {
|
|
1326
|
+
const ca = parseInt(el.getAttribute("colAddr") || "", 10);
|
|
1327
|
+
const ra = parseInt(el.getAttribute("rowAddr") || "", 10);
|
|
1328
|
+
if (!isNaN(ca)) tableCtx.cell.colAddr = ca;
|
|
1329
|
+
if (!isNaN(ra)) tableCtx.cell.rowAddr = ra;
|
|
1330
|
+
}
|
|
1331
|
+
break;
|
|
1332
|
+
case "cellSpan":
|
|
1333
|
+
if (tableCtx?.cell) {
|
|
1334
|
+
const rawCs = parseInt(el.getAttribute("colSpan") || "1", 10);
|
|
1335
|
+
const cs = isNaN(rawCs) ? 1 : rawCs;
|
|
1336
|
+
const rawRs = parseInt(el.getAttribute("rowSpan") || "1", 10);
|
|
1337
|
+
const rs = isNaN(rawRs) ? 1 : rawRs;
|
|
1338
|
+
tableCtx.cell.colSpan = clampSpan(cs, MAX_COLS);
|
|
1339
|
+
tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
|
|
1340
|
+
}
|
|
1341
|
+
break;
|
|
1342
|
+
case "p": {
|
|
1343
|
+
const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
|
|
1344
|
+
let text = rawText;
|
|
1345
|
+
let headingLevel;
|
|
1346
|
+
if (text) {
|
|
1347
|
+
const ph = resolveParaHeading(el, ctx);
|
|
1348
|
+
if (ph?.prefix) text = ph.prefix + " " + text;
|
|
1349
|
+
headingLevel = ph?.headingLevel;
|
|
1350
|
+
}
|
|
1351
|
+
if (text) {
|
|
1352
|
+
if (tableCtx?.cell) {
|
|
1353
|
+
const cell = tableCtx.cell;
|
|
1354
|
+
if (footnote) text += ` (\uC8FC: ${footnote})`;
|
|
1355
|
+
cell.text += (cell.text ? "\n" : "") + text;
|
|
1356
|
+
(cell.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
|
|
1357
|
+
} else if (!tableCtx) {
|
|
1358
|
+
const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
|
|
1359
|
+
if (headingLevel) block.level = headingLevel;
|
|
1360
|
+
if (style) block.style = style;
|
|
1361
|
+
if (href) block.href = href;
|
|
1362
|
+
if (footnote) block.footnoteText = footnote;
|
|
1363
|
+
blocks.push(block);
|
|
1364
|
+
} else {
|
|
1365
|
+
blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
|
|
1236
1366
|
}
|
|
1237
1367
|
}
|
|
1368
|
+
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1369
|
+
break;
|
|
1238
1370
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const mimeType = imageExtToMime(ext);
|
|
1250
|
-
imageIndex++;
|
|
1251
|
-
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
1252
|
-
img = { filename, data, mimeType };
|
|
1253
|
-
images.push(img);
|
|
1254
|
-
break;
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
if (err instanceof KordocError) throw err;
|
|
1371
|
+
// 이미지/그림/글상자 — 이미지·텍스트·캡션 병행 추출
|
|
1372
|
+
case "pic":
|
|
1373
|
+
case "shape":
|
|
1374
|
+
case "drawingObject": {
|
|
1375
|
+
if (tableCtx?.cell) {
|
|
1376
|
+
const sink = [];
|
|
1377
|
+
handleShape(el, sink, ctx);
|
|
1378
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1379
|
+
} else {
|
|
1380
|
+
handleShape(el, blocks, ctx);
|
|
1257
1381
|
}
|
|
1382
|
+
break;
|
|
1258
1383
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1384
|
+
// 메모 — 본문 혼입 차단 (v3.0)
|
|
1385
|
+
case "memogroup":
|
|
1386
|
+
case "memo": {
|
|
1387
|
+
if (ctx.warnings && extractTextFromNode(el)) {
|
|
1388
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
|
|
1389
|
+
}
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
default:
|
|
1393
|
+
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1394
|
+
break;
|
|
1261
1395
|
}
|
|
1262
|
-
if (!img) {
|
|
1263
|
-
block.type = "paragraph";
|
|
1264
|
-
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
1265
|
-
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, `[\uC774\uBBF8\uC9C0: ${ref}]`);
|
|
1266
|
-
continue;
|
|
1267
|
-
}
|
|
1268
|
-
block.text = img.filename;
|
|
1269
|
-
block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
|
|
1270
|
-
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, ``);
|
|
1271
1396
|
}
|
|
1272
|
-
return images;
|
|
1273
1397
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1398
|
+
function handleShape(el, sink, ctx) {
|
|
1399
|
+
const imgRef = extractImageRef(el);
|
|
1400
|
+
const drawTextChild = findDescendant(el, "drawText");
|
|
1401
|
+
if (imgRef) {
|
|
1402
|
+
const block = { type: "image", text: imgRef, pageNumber: ctx.sectionNum };
|
|
1403
|
+
const alt = userShapeComment(el);
|
|
1404
|
+
if (alt) block.footnoteText = alt;
|
|
1405
|
+
sink.push(block);
|
|
1406
|
+
}
|
|
1407
|
+
if (drawTextChild) {
|
|
1408
|
+
extractDrawTextBlocks(drawTextChild, sink, ctx);
|
|
1409
|
+
}
|
|
1410
|
+
const capEl = findChildByLocalName(el, "caption");
|
|
1411
|
+
if (capEl) {
|
|
1412
|
+
const capText = collectSubListText(capEl, ctx);
|
|
1413
|
+
if (capText) sink.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
|
|
1414
|
+
}
|
|
1415
|
+
if (!imgRef && !drawTextChild && ctx.warnings && ctx.sectionNum) {
|
|
1416
|
+
const localTag = (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
|
|
1417
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
1289
1418
|
}
|
|
1290
1419
|
}
|
|
1291
|
-
function
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1420
|
+
function userShapeComment(el) {
|
|
1421
|
+
const commentEl = findChildByLocalName(el, "shapeComment");
|
|
1422
|
+
if (!commentEl) return void 0;
|
|
1423
|
+
const text = extractTextFromNode(commentEl);
|
|
1424
|
+
if (!text) return void 0;
|
|
1425
|
+
if (/^그림입니다/.test(text)) return void 0;
|
|
1426
|
+
if (/^(?:모서리가 둥근 |둥근 )?[^\n]{1,20}입니다\.?$/.test(text)) return void 0;
|
|
1427
|
+
return text;
|
|
1428
|
+
}
|
|
1429
|
+
function mergeBlocksIntoCell(cell, sink) {
|
|
1430
|
+
for (const b of sink) {
|
|
1431
|
+
if ((b.type === "paragraph" || b.type === "heading") && b.text) {
|
|
1432
|
+
cell.text += (cell.text ? "\n" : "") + b.text;
|
|
1433
|
+
(cell.blocks ??= []).push(b);
|
|
1434
|
+
} else if (b.type === "image" || b.type === "table") {
|
|
1435
|
+
if (b.type === "image" && b.text) {
|
|
1436
|
+
cell.text += (cell.text ? "\n" : "") + ``;
|
|
1301
1437
|
}
|
|
1438
|
+
;
|
|
1439
|
+
(cell.blocks ??= []).push(b);
|
|
1440
|
+
cell.hasStructure = true;
|
|
1302
1441
|
}
|
|
1303
|
-
return void 0;
|
|
1304
|
-
};
|
|
1305
|
-
metadata.title = metadata.title || getText(["dc:title", "title"]);
|
|
1306
|
-
metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
|
|
1307
|
-
metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
|
|
1308
|
-
metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
|
|
1309
|
-
metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
|
|
1310
|
-
const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
|
|
1311
|
-
if (keywords && !metadata.keywords) {
|
|
1312
|
-
metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
1313
1442
|
}
|
|
1314
1443
|
}
|
|
1315
|
-
function
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
1329
|
-
pos++;
|
|
1330
|
-
while (pos < data.length - 30) {
|
|
1331
|
-
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
1332
|
-
pos++;
|
|
1333
|
-
}
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
1337
|
-
const method = view.getUint16(pos + 8, true);
|
|
1338
|
-
const compSize = view.getUint32(pos + 18, true);
|
|
1339
|
-
const nameLen = view.getUint16(pos + 26, true);
|
|
1340
|
-
const extraLen = view.getUint16(pos + 28, true);
|
|
1341
|
-
if (nameLen > 1024 || extraLen > 65535) {
|
|
1342
|
-
pos += 30 + nameLen + extraLen;
|
|
1343
|
-
continue;
|
|
1344
|
-
}
|
|
1345
|
-
const fileStart = pos + 30 + nameLen + extraLen;
|
|
1346
|
-
if (fileStart + compSize > data.length) break;
|
|
1347
|
-
if (compSize === 0 && method !== 0) {
|
|
1348
|
-
pos = fileStart;
|
|
1349
|
-
continue;
|
|
1350
|
-
}
|
|
1351
|
-
const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
|
|
1352
|
-
const name = new TextDecoder().decode(nameBytes);
|
|
1353
|
-
if (isPathTraversal(name)) {
|
|
1354
|
-
pos = fileStart + compSize;
|
|
1444
|
+
function collectSubListText(el, ctx, depth = 0) {
|
|
1445
|
+
if (depth > 10) return "";
|
|
1446
|
+
const parts = [];
|
|
1447
|
+
const children = el.childNodes;
|
|
1448
|
+
if (!children) return "";
|
|
1449
|
+
for (let i = 0; i < children.length; i++) {
|
|
1450
|
+
const ch = children[i];
|
|
1451
|
+
if (ch.nodeType !== 1) continue;
|
|
1452
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1453
|
+
if (tag === "p" || tag === "para") {
|
|
1454
|
+
const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
|
|
1455
|
+
if (t) parts.push(t);
|
|
1456
|
+
} else if (tag === "tbl") {
|
|
1355
1457
|
continue;
|
|
1458
|
+
} else {
|
|
1459
|
+
const t = collectSubListText(ch, ctx, depth + 1);
|
|
1460
|
+
if (t) parts.push(t);
|
|
1356
1461
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1462
|
+
}
|
|
1463
|
+
return parts.join("\n").trim();
|
|
1464
|
+
}
|
|
1465
|
+
function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1466
|
+
if (depth > MAX_XML_DEPTH) return tableCtx;
|
|
1467
|
+
const children = node.childNodes;
|
|
1468
|
+
if (!children) return tableCtx;
|
|
1469
|
+
const walkChildren = (parent, d) => {
|
|
1470
|
+
if (d > MAX_XML_DEPTH) return;
|
|
1471
|
+
const kids2 = parent.childNodes;
|
|
1472
|
+
if (!kids2) return;
|
|
1473
|
+
for (let i = 0; i < kids2.length; i++) {
|
|
1474
|
+
const el = kids2[i];
|
|
1475
|
+
if (el.nodeType !== 1) continue;
|
|
1476
|
+
const tag = el.tagName || el.localName || "";
|
|
1477
|
+
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1478
|
+
if (localTag === "tbl") {
|
|
1479
|
+
if (tableCtx) tableStack.push(tableCtx);
|
|
1480
|
+
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1481
|
+
walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
|
|
1482
|
+
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1483
|
+
} else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
|
|
1484
|
+
if (tableCtx?.cell) {
|
|
1485
|
+
const sink = [];
|
|
1486
|
+
handleShape(el, sink, ctx);
|
|
1487
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1488
|
+
} else {
|
|
1489
|
+
handleShape(el, blocks, ctx);
|
|
1490
|
+
}
|
|
1491
|
+
} else if (localTag === "drawText") {
|
|
1492
|
+
if (tableCtx?.cell) {
|
|
1493
|
+
const sink = [];
|
|
1494
|
+
extractDrawTextBlocks(el, sink, ctx);
|
|
1495
|
+
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1496
|
+
} else {
|
|
1497
|
+
extractDrawTextBlocks(el, blocks, ctx);
|
|
1498
|
+
}
|
|
1499
|
+
} else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
|
|
1500
|
+
walkChildren(el, d + 1);
|
|
1369
1501
|
}
|
|
1370
|
-
totalDecompressed += content.length * 2;
|
|
1371
|
-
if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
|
|
1372
|
-
sectionNum++;
|
|
1373
|
-
blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
|
|
1374
|
-
} catch {
|
|
1375
|
-
continue;
|
|
1376
1502
|
}
|
|
1503
|
+
};
|
|
1504
|
+
walkChildren(node, depth);
|
|
1505
|
+
return tableCtx;
|
|
1506
|
+
}
|
|
1507
|
+
function findDescendant(node, targetTag, depth = 0) {
|
|
1508
|
+
if (depth > 5) return null;
|
|
1509
|
+
const children = node.childNodes;
|
|
1510
|
+
if (!children) return null;
|
|
1511
|
+
for (let i = 0; i < children.length; i++) {
|
|
1512
|
+
const child = children[i];
|
|
1513
|
+
if (child.nodeType !== 1) continue;
|
|
1514
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1515
|
+
if (tag === targetTag) return child;
|
|
1516
|
+
const found = findDescendant(child, targetTag, depth + 1);
|
|
1517
|
+
if (found) return found;
|
|
1377
1518
|
}
|
|
1378
|
-
|
|
1379
|
-
applyPageText(blocks, shared);
|
|
1380
|
-
const markdown = blocksToMarkdown(blocks);
|
|
1381
|
-
return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1519
|
+
return null;
|
|
1382
1520
|
}
|
|
1383
|
-
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
if (
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1521
|
+
function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
|
|
1522
|
+
const children = drawTextNode.childNodes;
|
|
1523
|
+
if (!children) return;
|
|
1524
|
+
for (let i = 0; i < children.length; i++) {
|
|
1525
|
+
const child = children[i];
|
|
1526
|
+
if (child.nodeType !== 1) continue;
|
|
1527
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1528
|
+
if (tag === "subList" || tag === "p" || tag === "para") {
|
|
1529
|
+
if (tag === "subList") {
|
|
1530
|
+
extractDrawTextBlocks(child, blocks, ctx);
|
|
1531
|
+
} else {
|
|
1532
|
+
const info = extractParagraphInfo(child, ctx.styleMap, ctx);
|
|
1533
|
+
let text = info.text.trim();
|
|
1534
|
+
if (text) {
|
|
1535
|
+
const ph = resolveParaHeading(child, ctx);
|
|
1536
|
+
if (ph?.prefix) text = ph.prefix + " " + text;
|
|
1537
|
+
const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
|
|
1538
|
+
if (info.href) block.href = info.href;
|
|
1539
|
+
if (info.footnote) block.footnoteText = info.footnote;
|
|
1540
|
+
blocks.push(block);
|
|
1541
|
+
}
|
|
1542
|
+
walkParagraphChildren(child, blocks, null, [], ctx);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1392
1545
|
}
|
|
1393
|
-
const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
|
|
1394
|
-
return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
|
|
1395
1546
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
for (let i = 0; i <
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
const
|
|
1406
|
-
if (
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (href) ordered.push(href);
|
|
1413
|
-
}
|
|
1414
|
-
if (ordered.length > 0) return ordered;
|
|
1547
|
+
function extractHyperlinkHref(fieldBegin) {
|
|
1548
|
+
if ((fieldBegin.getAttribute("type") || "").toUpperCase() !== "HYPERLINK") return void 0;
|
|
1549
|
+
const params = findChildByLocalName(fieldBegin, "parameters");
|
|
1550
|
+
if (!params) return void 0;
|
|
1551
|
+
const children = params.childNodes;
|
|
1552
|
+
if (!children) return void 0;
|
|
1553
|
+
for (let i = 0; i < children.length; i++) {
|
|
1554
|
+
const ch = children[i];
|
|
1555
|
+
if (ch.nodeType !== 1) continue;
|
|
1556
|
+
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1557
|
+
if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
|
|
1558
|
+
let url = (ch.textContent || "").trim();
|
|
1559
|
+
if (!url) continue;
|
|
1560
|
+
url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
|
|
1561
|
+
const safe = sanitizeHref(url);
|
|
1562
|
+
if (safe) return safe;
|
|
1415
1563
|
}
|
|
1416
|
-
return
|
|
1564
|
+
return void 0;
|
|
1417
1565
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
let baseFontSize = 0;
|
|
1421
|
-
const sizeFreq = /* @__PURE__ */ new Map();
|
|
1422
|
-
for (const b of blocks) {
|
|
1423
|
-
if (b.style?.fontSize) {
|
|
1424
|
-
sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
let maxCount = 0;
|
|
1428
|
-
for (const [size, count] of sizeFreq) {
|
|
1429
|
-
if (count > maxCount) {
|
|
1430
|
-
maxCount = count;
|
|
1431
|
-
baseFontSize = size;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
for (const block of blocks) {
|
|
1435
|
-
if (block.type !== "paragraph" || !block.text) continue;
|
|
1436
|
-
const text = block.text.trim();
|
|
1437
|
-
if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
|
|
1438
|
-
let level = 0;
|
|
1439
|
-
if (baseFontSize > 0 && block.style?.fontSize) {
|
|
1440
|
-
const ratio = block.style.fontSize / baseFontSize;
|
|
1441
|
-
if (ratio >= HEADING_RATIO_H1) level = 1;
|
|
1442
|
-
else if (ratio >= HEADING_RATIO_H2) level = 2;
|
|
1443
|
-
else if (ratio >= HEADING_RATIO_H3) level = 3;
|
|
1444
|
-
}
|
|
1445
|
-
const compactText = text.replace(/\s+/g, "");
|
|
1446
|
-
if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
|
|
1447
|
-
if (level === 0) level = 3;
|
|
1448
|
-
}
|
|
1449
|
-
if (level > 0) {
|
|
1450
|
-
block.type = "heading";
|
|
1451
|
-
block.level = level;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1566
|
+
function isInDeletedRange(ctx) {
|
|
1567
|
+
return (ctx?.shared.track.deleteDepth ?? 0) > 0;
|
|
1454
1568
|
}
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1569
|
+
function extractParagraphInfo(para, styleMap, ctx) {
|
|
1570
|
+
let text = "";
|
|
1571
|
+
let href;
|
|
1572
|
+
let footnote;
|
|
1573
|
+
let charPrId;
|
|
1574
|
+
const handleCtrl = (ctrlEl) => {
|
|
1575
|
+
const kids2 = ctrlEl.childNodes;
|
|
1576
|
+
if (!kids2) return;
|
|
1577
|
+
for (let j = 0; j < kids2.length; j++) {
|
|
1578
|
+
const k = kids2[j];
|
|
1579
|
+
if (k.nodeType !== 1) continue;
|
|
1580
|
+
const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
|
|
1581
|
+
switch (ktag) {
|
|
1582
|
+
// 머리말/꼬리말 — 문서당 1회 수집, 본문 앞/뒤 배치
|
|
1583
|
+
case "header":
|
|
1584
|
+
case "footer": {
|
|
1585
|
+
if (!ctx) break;
|
|
1586
|
+
const t = collectSubListText(k, ctx);
|
|
1587
|
+
if (t) {
|
|
1588
|
+
const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
|
|
1589
|
+
if (!bucket.includes(t)) bucket.push(t);
|
|
1590
|
+
}
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
// 각주/미주 — 해당 문단의 footnote로 인라인 보존
|
|
1594
|
+
case "footNote":
|
|
1595
|
+
case "endNote": {
|
|
1596
|
+
const noteText = extractTextFromNode(k);
|
|
1597
|
+
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
// 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
|
|
1601
|
+
case "fieldBegin": {
|
|
1602
|
+
const url = extractHyperlinkHref(k);
|
|
1603
|
+
if (url && !href) href = url;
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
case "fieldEnd":
|
|
1607
|
+
break;
|
|
1608
|
+
// 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
|
|
1609
|
+
case "deleteBegin":
|
|
1610
|
+
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1611
|
+
break;
|
|
1612
|
+
case "deleteEnd":
|
|
1613
|
+
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1614
|
+
break;
|
|
1615
|
+
case "insertBegin":
|
|
1616
|
+
case "insertEnd":
|
|
1617
|
+
break;
|
|
1618
|
+
// 삽입분은 최종본에 포함
|
|
1619
|
+
// 숨은 설명 — 본문 혼입 차단
|
|
1620
|
+
case "hiddenComment": {
|
|
1621
|
+
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1622
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
|
|
1623
|
+
}
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
// 콘텐츠 없는 제어 요소 — 스킵
|
|
1627
|
+
case "bookmark":
|
|
1628
|
+
case "pageNum":
|
|
1629
|
+
case "pageNumCtrl":
|
|
1630
|
+
case "pageHiding":
|
|
1631
|
+
case "newNum":
|
|
1632
|
+
case "autoNum":
|
|
1633
|
+
case "indexmark":
|
|
1634
|
+
case "colPr":
|
|
1635
|
+
break;
|
|
1636
|
+
// 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
|
|
1637
|
+
default: {
|
|
1638
|
+
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1639
|
+
ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
|
|
1470
1640
|
}
|
|
1471
1641
|
}
|
|
1472
|
-
anchors.push(cell);
|
|
1473
|
-
c += cell.colSpan - 1;
|
|
1474
1642
|
}
|
|
1475
1643
|
}
|
|
1476
|
-
}
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
|
|
1489
|
-
const cand = table.cells[src.rowAddr]?.[src.colAddr];
|
|
1490
|
-
if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
|
|
1491
|
-
}
|
|
1492
|
-
if (!target) {
|
|
1493
|
-
outer: for (const irRow of table.cells) {
|
|
1494
|
-
for (const cand of irRow) {
|
|
1495
|
-
if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
|
|
1496
|
-
target = cand;
|
|
1497
|
-
break outer;
|
|
1498
|
-
}
|
|
1644
|
+
};
|
|
1645
|
+
const walk = (node) => {
|
|
1646
|
+
const children = node.childNodes;
|
|
1647
|
+
if (!children) return;
|
|
1648
|
+
for (let i = 0; i < children.length; i++) {
|
|
1649
|
+
const child = children[i];
|
|
1650
|
+
if (child.nodeType === 3) {
|
|
1651
|
+
const t = child.textContent || "";
|
|
1652
|
+
if (isInDeletedRange(ctx)) {
|
|
1653
|
+
if (t && ctx && !ctx.shared.track.warned) {
|
|
1654
|
+
ctx.shared.track.warned = true;
|
|
1655
|
+
ctx.warnings?.push({ page: ctx.sectionNum, message: "\uBCC0\uACBD\uCD94\uC801 \uC0AD\uC81C \uD14D\uC2A4\uD2B8 \uCD9C\uB825 \uC81C\uC678", code: "HIDDEN_TEXT_FILTERED" });
|
|
1499
1656
|
}
|
|
1657
|
+
} else {
|
|
1658
|
+
text += t;
|
|
1500
1659
|
}
|
|
1660
|
+
continue;
|
|
1501
1661
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
|
|
1516
|
-
if (newTable.rows.length === 0) {
|
|
1517
|
-
if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
|
|
1518
|
-
return parentTable;
|
|
1519
|
-
}
|
|
1520
|
-
const ir = buildTableWithCellMeta(newTable);
|
|
1521
|
-
const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
|
|
1522
|
-
if (parentTable?.cell) {
|
|
1523
|
-
const cell = parentTable.cell;
|
|
1524
|
-
(cell.blocks ??= []).push(block);
|
|
1525
|
-
cell.hasStructure = true;
|
|
1526
|
-
let flat = convertTableToText(newTable.rows);
|
|
1527
|
-
if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
|
|
1528
|
-
if (flat) cell.text += (cell.text ? "\n" : "") + flat;
|
|
1529
|
-
} else {
|
|
1530
|
-
blocks.push(block);
|
|
1531
|
-
}
|
|
1532
|
-
return parentTable;
|
|
1533
|
-
}
|
|
1534
|
-
function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
|
|
1535
|
-
const parser = createXmlParser(warnings);
|
|
1536
|
-
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
1537
|
-
if (!doc.documentElement) return [];
|
|
1538
|
-
const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
|
|
1539
|
-
ctx.shared.track.deleteDepth = 0;
|
|
1540
|
-
for (const tagName of ["hp:secPr", "secPr"]) {
|
|
1541
|
-
const els = doc.getElementsByTagName(tagName);
|
|
1542
|
-
if (els.length > 0) {
|
|
1543
|
-
const v = els[0].getAttribute("outlineShapeIDRef");
|
|
1544
|
-
if (v) ctx.outlineNumId = v;
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
const blocks = [];
|
|
1549
|
-
walkSection(doc.documentElement, blocks, null, [], ctx);
|
|
1550
|
-
return blocks;
|
|
1551
|
-
}
|
|
1552
|
-
function extractImageRef(el) {
|
|
1553
|
-
const children = el.childNodes;
|
|
1554
|
-
if (!children) return null;
|
|
1555
|
-
for (let i = 0; i < children.length; i++) {
|
|
1556
|
-
const child = children[i];
|
|
1557
|
-
if (child.nodeType !== 1) continue;
|
|
1558
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1559
|
-
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
1560
|
-
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
1561
|
-
if (ref) return ref;
|
|
1562
|
-
}
|
|
1563
|
-
const nested = extractImageRef(child);
|
|
1564
|
-
if (nested) return nested;
|
|
1565
|
-
}
|
|
1566
|
-
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
1567
|
-
if (directRef) return directRef;
|
|
1568
|
-
return null;
|
|
1569
|
-
}
|
|
1570
|
-
function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
|
|
1571
|
-
if (depth > MAX_XML_DEPTH) return;
|
|
1572
|
-
const children = node.childNodes;
|
|
1573
|
-
if (!children) return;
|
|
1574
|
-
for (let i = 0; i < children.length; i++) {
|
|
1575
|
-
const el = children[i];
|
|
1576
|
-
if (el.nodeType !== 1) continue;
|
|
1577
|
-
const tag = el.tagName || el.localName || "";
|
|
1578
|
-
const localTag = tag.replace(/^[^:]+:/, "");
|
|
1579
|
-
switch (localTag) {
|
|
1580
|
-
case "tbl": {
|
|
1581
|
-
if (tableCtx) tableStack.push(tableCtx);
|
|
1582
|
-
const newTable = { rows: [], currentRow: [], cell: null };
|
|
1583
|
-
walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
|
|
1584
|
-
tableCtx = completeTable(newTable, tableStack, blocks, ctx);
|
|
1585
|
-
break;
|
|
1586
|
-
}
|
|
1587
|
-
// 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
|
|
1588
|
-
case "caption":
|
|
1589
|
-
if (tableCtx) {
|
|
1590
|
-
const capText = collectSubListText(el, ctx);
|
|
1591
|
-
if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
|
|
1592
|
-
}
|
|
1593
|
-
break;
|
|
1594
|
-
case "tr":
|
|
1595
|
-
if (tableCtx) {
|
|
1596
|
-
tableCtx.currentRow = [];
|
|
1597
|
-
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1598
|
-
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
1599
|
-
tableCtx.currentRow = [];
|
|
1600
|
-
}
|
|
1601
|
-
break;
|
|
1602
|
-
case "tc":
|
|
1603
|
-
if (tableCtx) {
|
|
1604
|
-
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1605
|
-
if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
|
|
1606
|
-
walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
|
|
1607
|
-
if (tableCtx.cell) {
|
|
1608
|
-
tableCtx.currentRow.push(tableCtx.cell);
|
|
1609
|
-
tableCtx.cell = null;
|
|
1662
|
+
if (child.nodeType !== 1) continue;
|
|
1663
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1664
|
+
switch (tag) {
|
|
1665
|
+
case "t":
|
|
1666
|
+
walk(child);
|
|
1667
|
+
break;
|
|
1668
|
+
// 자식 순회 (tab 등 하위 요소 처리)
|
|
1669
|
+
case "tab": {
|
|
1670
|
+
const leader = child.getAttribute("leader");
|
|
1671
|
+
if (leader && leader !== "0") {
|
|
1672
|
+
text += "";
|
|
1673
|
+
} else {
|
|
1674
|
+
text += " ";
|
|
1610
1675
|
}
|
|
1676
|
+
break;
|
|
1611
1677
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1678
|
+
case "br":
|
|
1679
|
+
if ((child.getAttribute("type") || "line") === "line") text += "\n";
|
|
1680
|
+
break;
|
|
1681
|
+
case "lineBreak":
|
|
1682
|
+
text += "\n";
|
|
1683
|
+
break;
|
|
1684
|
+
// 강제 줄바꿈 — ref 추출기·소스맵 스캐너와 동일 모델
|
|
1685
|
+
case "fwSpace":
|
|
1686
|
+
case "hwSpace":
|
|
1687
|
+
text += " ";
|
|
1688
|
+
break;
|
|
1689
|
+
case "tbl":
|
|
1690
|
+
break;
|
|
1691
|
+
// 테이블은 walkSection에서 처리
|
|
1692
|
+
// 하이퍼링크
|
|
1693
|
+
case "hyperlink": {
|
|
1694
|
+
const url = child.getAttribute("url") || child.getAttribute("href") || "";
|
|
1695
|
+
if (url) {
|
|
1696
|
+
const safe = sanitizeHref(url);
|
|
1697
|
+
if (safe) href = safe;
|
|
1698
|
+
}
|
|
1699
|
+
walk(child);
|
|
1700
|
+
break;
|
|
1619
1701
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
|
|
1702
|
+
// 각주/미주
|
|
1703
|
+
case "footNote":
|
|
1704
|
+
case "endNote":
|
|
1705
|
+
case "fn":
|
|
1706
|
+
case "en": {
|
|
1707
|
+
const noteText = extractTextFromNode(child);
|
|
1708
|
+
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1709
|
+
break;
|
|
1629
1710
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1711
|
+
// 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
|
|
1712
|
+
case "ctrl":
|
|
1713
|
+
handleCtrl(child);
|
|
1714
|
+
break;
|
|
1715
|
+
// run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
|
|
1716
|
+
case "fieldBegin": {
|
|
1717
|
+
const url = extractHyperlinkHref(child);
|
|
1718
|
+
if (url && !href) href = url;
|
|
1719
|
+
break;
|
|
1639
1720
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1721
|
+
// run 직계 변경추적 마커 (비표준 경로)
|
|
1722
|
+
case "deleteBegin":
|
|
1723
|
+
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1724
|
+
break;
|
|
1725
|
+
case "deleteEnd":
|
|
1726
|
+
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1727
|
+
break;
|
|
1728
|
+
case "insertBegin":
|
|
1729
|
+
case "insertEnd":
|
|
1730
|
+
break;
|
|
1731
|
+
case "fieldEnd":
|
|
1732
|
+
case "parameters":
|
|
1733
|
+
case "stringParam":
|
|
1734
|
+
case "integerParam":
|
|
1735
|
+
case "boolParam":
|
|
1736
|
+
case "floatParam":
|
|
1737
|
+
case "secPr":
|
|
1738
|
+
// 섹션 속성 (페이지 설정 등)
|
|
1739
|
+
case "colPr":
|
|
1740
|
+
// 다단 속성
|
|
1741
|
+
case "linesegarray":
|
|
1742
|
+
case "lineseg":
|
|
1743
|
+
// 레이아웃 정보
|
|
1744
|
+
// 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
|
|
1745
|
+
case "pic":
|
|
1746
|
+
case "shape":
|
|
1747
|
+
case "drawingObject":
|
|
1748
|
+
case "shapeComment":
|
|
1749
|
+
case "drawText":
|
|
1750
|
+
break;
|
|
1751
|
+
// 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
|
|
1752
|
+
// 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
|
|
1753
|
+
// 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
|
|
1754
|
+
case "equation": {
|
|
1755
|
+
const script = findChildByLocalName(child, "script");
|
|
1756
|
+
const raw = script ? extractTextFromNode(script) : "";
|
|
1757
|
+
if (raw.trim()) {
|
|
1758
|
+
try {
|
|
1759
|
+
const latex = hmlToLatex(raw).trim();
|
|
1760
|
+
if (latex) text += " $" + latex + "$ ";
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1655
1763
|
}
|
|
1764
|
+
break;
|
|
1656
1765
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
case "drawingObject": {
|
|
1664
|
-
if (tableCtx?.cell) {
|
|
1665
|
-
const sink = [];
|
|
1666
|
-
handleShape(el, sink, ctx);
|
|
1667
|
-
mergeBlocksIntoCell(tableCtx.cell, sink);
|
|
1668
|
-
} else {
|
|
1669
|
-
handleShape(el, blocks, ctx);
|
|
1670
|
-
}
|
|
1671
|
-
break;
|
|
1672
|
-
}
|
|
1673
|
-
// 메모 — 본문 혼입 차단 (v3.0)
|
|
1674
|
-
case "memogroup":
|
|
1675
|
-
case "memo": {
|
|
1676
|
-
if (ctx.warnings && extractTextFromNode(el)) {
|
|
1677
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
|
|
1766
|
+
// run 요소에서 charPrIDRef 추출
|
|
1767
|
+
case "r": {
|
|
1768
|
+
const runCharPr = child.getAttribute("charPrIDRef");
|
|
1769
|
+
if (runCharPr && !charPrId) charPrId = runCharPr;
|
|
1770
|
+
walk(child);
|
|
1771
|
+
break;
|
|
1678
1772
|
}
|
|
1679
|
-
|
|
1773
|
+
default:
|
|
1774
|
+
walk(child);
|
|
1775
|
+
break;
|
|
1680
1776
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
walk(para);
|
|
1780
|
+
const leaderIdx = text.indexOf("");
|
|
1781
|
+
if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
|
|
1782
|
+
let cleanText = text.replace(/[ \t]+/g, " ").trim();
|
|
1783
|
+
if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
|
|
1784
|
+
cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
|
|
1785
|
+
cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
|
|
1786
|
+
let style;
|
|
1787
|
+
if (styleMap && charPrId) {
|
|
1788
|
+
const charProp = styleMap.charProperties.get(charPrId);
|
|
1789
|
+
if (charProp) {
|
|
1790
|
+
style = {};
|
|
1791
|
+
if (charProp.fontSize) style.fontSize = charProp.fontSize;
|
|
1792
|
+
if (charProp.bold) style.bold = true;
|
|
1793
|
+
if (charProp.italic) style.italic = true;
|
|
1794
|
+
if (charProp.fontName) style.fontName = charProp.fontName;
|
|
1795
|
+
if (!style.fontSize && !style.bold && !style.italic) style = void 0;
|
|
1684
1796
|
}
|
|
1685
1797
|
}
|
|
1798
|
+
return { text: cleanText, href, footnote, style };
|
|
1686
1799
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1800
|
+
|
|
1801
|
+
// src/hwpx/images.ts
|
|
1802
|
+
function imageExtToMime(ext) {
|
|
1803
|
+
switch (ext.toLowerCase()) {
|
|
1804
|
+
case "jpg":
|
|
1805
|
+
case "jpeg":
|
|
1806
|
+
return "image/jpeg";
|
|
1807
|
+
case "png":
|
|
1808
|
+
return "image/png";
|
|
1809
|
+
case "gif":
|
|
1810
|
+
return "image/gif";
|
|
1811
|
+
case "bmp":
|
|
1812
|
+
return "image/bmp";
|
|
1813
|
+
case "tif":
|
|
1814
|
+
case "tiff":
|
|
1815
|
+
return "image/tiff";
|
|
1816
|
+
case "wmf":
|
|
1817
|
+
return "image/wmf";
|
|
1818
|
+
case "emf":
|
|
1819
|
+
return "image/emf";
|
|
1820
|
+
case "svg":
|
|
1821
|
+
return "image/svg+xml";
|
|
1822
|
+
default:
|
|
1823
|
+
return "application/octet-stream";
|
|
1707
1824
|
}
|
|
1708
1825
|
}
|
|
1709
|
-
function
|
|
1710
|
-
|
|
1711
|
-
if (
|
|
1712
|
-
|
|
1713
|
-
if (
|
|
1714
|
-
if (
|
|
1715
|
-
if (
|
|
1716
|
-
return
|
|
1826
|
+
function mimeToExt(mime) {
|
|
1827
|
+
if (mime.includes("jpeg")) return "jpg";
|
|
1828
|
+
if (mime.includes("png")) return "png";
|
|
1829
|
+
if (mime.includes("gif")) return "gif";
|
|
1830
|
+
if (mime.includes("bmp")) return "bmp";
|
|
1831
|
+
if (mime.includes("tiff")) return "tif";
|
|
1832
|
+
if (mime.includes("wmf")) return "wmf";
|
|
1833
|
+
if (mime.includes("emf")) return "emf";
|
|
1834
|
+
if (mime.includes("svg")) return "svg";
|
|
1835
|
+
return "bin";
|
|
1717
1836
|
}
|
|
1718
|
-
function
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
(
|
|
1723
|
-
} else if (
|
|
1724
|
-
|
|
1725
|
-
|
|
1837
|
+
function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
|
|
1838
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
1839
|
+
for (const block of blocks) {
|
|
1840
|
+
if (block.type === "image") {
|
|
1841
|
+
out.push({ block, ownerCell });
|
|
1842
|
+
} else if (block.type === "table" && block.table) {
|
|
1843
|
+
for (const row of block.table.cells) {
|
|
1844
|
+
for (const cell of row) {
|
|
1845
|
+
if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
|
|
1846
|
+
}
|
|
1726
1847
|
}
|
|
1727
|
-
;
|
|
1728
|
-
(cell.blocks ??= []).push(b);
|
|
1729
|
-
cell.hasStructure = true;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
function collectSubListText(el, ctx, depth = 0) {
|
|
1734
|
-
if (depth > 10) return "";
|
|
1735
|
-
const parts = [];
|
|
1736
|
-
const children = el.childNodes;
|
|
1737
|
-
if (!children) return "";
|
|
1738
|
-
for (let i = 0; i < children.length; i++) {
|
|
1739
|
-
const ch = children[i];
|
|
1740
|
-
if (ch.nodeType !== 1) continue;
|
|
1741
|
-
const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
|
|
1742
|
-
if (tag === "p" || tag === "para") {
|
|
1743
|
-
const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
|
|
1744
|
-
if (t) parts.push(t);
|
|
1745
|
-
} else if (tag === "tbl") {
|
|
1746
|
-
continue;
|
|
1747
|
-
} else {
|
|
1748
|
-
const t = collectSubListText(ch, ctx, depth + 1);
|
|
1749
|
-
if (t) parts.push(t);
|
|
1750
1848
|
}
|
|
1751
1849
|
}
|
|
1752
|
-
return parts.join("\n").trim();
|
|
1753
1850
|
}
|
|
1754
|
-
function
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if (!
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
const
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1851
|
+
async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
|
|
1852
|
+
const images = [];
|
|
1853
|
+
let imageIndex = 0;
|
|
1854
|
+
const imageBlocks = [];
|
|
1855
|
+
collectImageBlocks(blocks, imageBlocks);
|
|
1856
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1857
|
+
for (const { block, ownerCell } of imageBlocks) {
|
|
1858
|
+
if (block.type !== "image" || !block.text) continue;
|
|
1859
|
+
const ref = block.text;
|
|
1860
|
+
let img = resolved.get(ref);
|
|
1861
|
+
if (img === void 0) {
|
|
1862
|
+
img = null;
|
|
1863
|
+
const candidates = [
|
|
1864
|
+
`BinData/${ref}`,
|
|
1865
|
+
`Contents/BinData/${ref}`,
|
|
1866
|
+
ref
|
|
1867
|
+
// 절대 경로일 수도 있음
|
|
1868
|
+
];
|
|
1869
|
+
let resolvedPath = null;
|
|
1870
|
+
if (!ref.includes(".")) {
|
|
1871
|
+
const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
|
|
1872
|
+
for (const prefix of prefixes) {
|
|
1873
|
+
const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
|
|
1874
|
+
if (match.length > 0) {
|
|
1875
|
+
resolvedPath = match[0].name;
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1779
1878
|
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1879
|
+
}
|
|
1880
|
+
const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
|
|
1881
|
+
for (const path of allCandidates) {
|
|
1882
|
+
if (isPathTraversal(path)) continue;
|
|
1883
|
+
const file = zip.file(path);
|
|
1884
|
+
if (!file) continue;
|
|
1885
|
+
try {
|
|
1886
|
+
const data = await file.async("uint8array");
|
|
1887
|
+
decompressed.total += data.length;
|
|
1888
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
1889
|
+
const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
|
|
1890
|
+
const mimeType = imageExtToMime(ext);
|
|
1891
|
+
imageIndex++;
|
|
1892
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
1893
|
+
img = { filename, data, mimeType };
|
|
1894
|
+
images.push(img);
|
|
1895
|
+
break;
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
if (err instanceof KordocError) throw err;
|
|
1787
1898
|
}
|
|
1788
|
-
} else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
|
|
1789
|
-
walkChildren(el, d + 1);
|
|
1790
1899
|
}
|
|
1900
|
+
if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
|
|
1901
|
+
resolved.set(ref, img);
|
|
1791
1902
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const child = children[i];
|
|
1802
|
-
if (child.nodeType !== 1) continue;
|
|
1803
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1804
|
-
if (tag === targetTag) return child;
|
|
1805
|
-
const found = findDescendant(child, targetTag, depth + 1);
|
|
1806
|
-
if (found) return found;
|
|
1903
|
+
if (!img) {
|
|
1904
|
+
block.type = "paragraph";
|
|
1905
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
1906
|
+
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, `[\uC774\uBBF8\uC9C0: ${ref}]`);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
block.text = img.filename;
|
|
1910
|
+
block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
|
|
1911
|
+
if (ownerCell) ownerCell.text = ownerCell.text.replace(``, ``);
|
|
1807
1912
|
}
|
|
1808
|
-
return
|
|
1913
|
+
return images;
|
|
1809
1914
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1915
|
+
|
|
1916
|
+
// src/hwpx/metadata.ts
|
|
1917
|
+
import JSZip2 from "jszip";
|
|
1918
|
+
|
|
1919
|
+
// src/hwpx/zip-sections.ts
|
|
1920
|
+
import { inflateRawSync } from "zlib";
|
|
1921
|
+
function extractFromBrokenZip(buffer) {
|
|
1922
|
+
const data = new Uint8Array(buffer);
|
|
1923
|
+
const view = new DataView(buffer);
|
|
1924
|
+
let pos = 0;
|
|
1925
|
+
const blocks = [];
|
|
1926
|
+
const warnings = [
|
|
1927
|
+
{ code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
|
|
1928
|
+
];
|
|
1929
|
+
let totalDecompressed = 0;
|
|
1930
|
+
let entryCount = 0;
|
|
1931
|
+
let sectionNum = 0;
|
|
1932
|
+
const shared = createSectionShared();
|
|
1933
|
+
while (pos < data.length - 30) {
|
|
1934
|
+
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
1935
|
+
pos++;
|
|
1936
|
+
while (pos < data.length - 30) {
|
|
1937
|
+
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
1938
|
+
pos++;
|
|
1832
1939
|
}
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
1943
|
+
const method = view.getUint16(pos + 8, true);
|
|
1944
|
+
const compSize = view.getUint32(pos + 18, true);
|
|
1945
|
+
const nameLen = view.getUint16(pos + 26, true);
|
|
1946
|
+
const extraLen = view.getUint16(pos + 28, true);
|
|
1947
|
+
if (nameLen > 1024 || extraLen > 65535) {
|
|
1948
|
+
pos += 30 + nameLen + extraLen;
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
const fileStart = pos + 30 + nameLen + extraLen;
|
|
1952
|
+
if (fileStart + compSize > data.length) break;
|
|
1953
|
+
if (compSize === 0 && method !== 0) {
|
|
1954
|
+
pos = fileStart;
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
|
|
1958
|
+
const name = new TextDecoder().decode(nameBytes);
|
|
1959
|
+
if (isPathTraversal(name)) {
|
|
1960
|
+
pos = fileStart + compSize;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
const fileData = data.slice(fileStart, fileStart + compSize);
|
|
1964
|
+
pos = fileStart + compSize;
|
|
1965
|
+
if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
|
|
1966
|
+
try {
|
|
1967
|
+
let content;
|
|
1968
|
+
if (method === 0) {
|
|
1969
|
+
content = new TextDecoder().decode(fileData);
|
|
1970
|
+
} else if (method === 8) {
|
|
1971
|
+
const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
|
|
1972
|
+
content = new TextDecoder().decode(decompressed);
|
|
1973
|
+
} else {
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
totalDecompressed += content.length * 2;
|
|
1977
|
+
if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
|
|
1978
|
+
sectionNum++;
|
|
1979
|
+
blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
|
|
1980
|
+
} catch {
|
|
1981
|
+
continue;
|
|
1833
1982
|
}
|
|
1834
1983
|
}
|
|
1984
|
+
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");
|
|
1985
|
+
applyPageText(blocks, shared);
|
|
1986
|
+
const markdown = blocksToMarkdown(blocks);
|
|
1987
|
+
return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1835
1988
|
}
|
|
1836
|
-
function
|
|
1837
|
-
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
const
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
if (!url) continue;
|
|
1849
|
-
url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
|
|
1850
|
-
const safe = sanitizeHref(url);
|
|
1851
|
-
if (safe) return safe;
|
|
1852
|
-
}
|
|
1853
|
-
return void 0;
|
|
1854
|
-
}
|
|
1855
|
-
function isInDeletedRange(ctx) {
|
|
1856
|
-
return (ctx?.shared.track.deleteDepth ?? 0) > 0;
|
|
1989
|
+
async function resolveSectionPaths(zip) {
|
|
1990
|
+
const manifestPaths = ["Contents/content.hpf", "content.hpf"];
|
|
1991
|
+
for (const mp of manifestPaths) {
|
|
1992
|
+
const mpLower = mp.toLowerCase();
|
|
1993
|
+
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
|
|
1994
|
+
if (!file) continue;
|
|
1995
|
+
const xml = await file.async("text");
|
|
1996
|
+
const paths = parseSectionPathsFromManifest(xml);
|
|
1997
|
+
if (paths.length > 0) return paths;
|
|
1998
|
+
}
|
|
1999
|
+
const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
|
|
2000
|
+
return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
|
|
1857
2001
|
}
|
|
1858
|
-
function
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
const t = collectSubListText(k, ctx);
|
|
1876
|
-
if (t) {
|
|
1877
|
-
const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
|
|
1878
|
-
if (!bucket.includes(t)) bucket.push(t);
|
|
1879
|
-
}
|
|
1880
|
-
break;
|
|
1881
|
-
}
|
|
1882
|
-
// 각주/미주 — 해당 문단의 footnote로 인라인 보존
|
|
1883
|
-
case "footNote":
|
|
1884
|
-
case "endNote": {
|
|
1885
|
-
const noteText = extractTextFromNode(k);
|
|
1886
|
-
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1887
|
-
break;
|
|
1888
|
-
}
|
|
1889
|
-
// 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
|
|
1890
|
-
case "fieldBegin": {
|
|
1891
|
-
const url = extractHyperlinkHref(k);
|
|
1892
|
-
if (url && !href) href = url;
|
|
1893
|
-
break;
|
|
1894
|
-
}
|
|
1895
|
-
case "fieldEnd":
|
|
1896
|
-
break;
|
|
1897
|
-
// 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
|
|
1898
|
-
case "deleteBegin":
|
|
1899
|
-
if (ctx) ctx.shared.track.deleteDepth++;
|
|
1900
|
-
break;
|
|
1901
|
-
case "deleteEnd":
|
|
1902
|
-
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
1903
|
-
break;
|
|
1904
|
-
case "insertBegin":
|
|
1905
|
-
case "insertEnd":
|
|
1906
|
-
break;
|
|
1907
|
-
// 삽입분은 최종본에 포함
|
|
1908
|
-
// 숨은 설명 — 본문 혼입 차단
|
|
1909
|
-
case "hiddenComment": {
|
|
1910
|
-
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1911
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
|
|
1912
|
-
}
|
|
1913
|
-
break;
|
|
1914
|
-
}
|
|
1915
|
-
// 콘텐츠 없는 제어 요소 — 스킵
|
|
1916
|
-
case "bookmark":
|
|
1917
|
-
case "pageNum":
|
|
1918
|
-
case "pageNumCtrl":
|
|
1919
|
-
case "pageHiding":
|
|
1920
|
-
case "newNum":
|
|
1921
|
-
case "autoNum":
|
|
1922
|
-
case "indexmark":
|
|
1923
|
-
case "colPr":
|
|
1924
|
-
break;
|
|
1925
|
-
// 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
|
|
1926
|
-
default: {
|
|
1927
|
-
if (ctx?.warnings && extractTextFromNode(k)) {
|
|
1928
|
-
ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
2002
|
+
function parseSectionPathsFromManifest(xml) {
|
|
2003
|
+
const parser = createXmlParser();
|
|
2004
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
2005
|
+
const items = doc.getElementsByTagName("opf:item");
|
|
2006
|
+
const spine = doc.getElementsByTagName("opf:itemref");
|
|
2007
|
+
const idToHref = /* @__PURE__ */ new Map();
|
|
2008
|
+
for (let i = 0; i < items.length; i++) {
|
|
2009
|
+
const item = items[i];
|
|
2010
|
+
const id = item.getAttribute("id") || "";
|
|
2011
|
+
const href = normalizeSectionHref(item.getAttribute("href") || "");
|
|
2012
|
+
if (id && href) idToHref.set(id, href);
|
|
2013
|
+
}
|
|
2014
|
+
if (spine.length > 0) {
|
|
2015
|
+
const ordered = [];
|
|
2016
|
+
for (let i = 0; i < spine.length; i++) {
|
|
2017
|
+
const href = idToHref.get(spine[i].getAttribute("idref") || "");
|
|
2018
|
+
if (href) ordered.push(href);
|
|
1932
2019
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
if (child.nodeType !== 1) continue;
|
|
1952
|
-
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
1953
|
-
switch (tag) {
|
|
1954
|
-
case "t":
|
|
1955
|
-
walk(child);
|
|
1956
|
-
break;
|
|
1957
|
-
// 자식 순회 (tab 등 하위 요소 처리)
|
|
1958
|
-
case "tab": {
|
|
1959
|
-
const leader = child.getAttribute("leader");
|
|
1960
|
-
if (leader && leader !== "0") {
|
|
1961
|
-
text += "";
|
|
1962
|
-
} else {
|
|
1963
|
-
text += " ";
|
|
1964
|
-
}
|
|
1965
|
-
break;
|
|
1966
|
-
}
|
|
1967
|
-
case "br":
|
|
1968
|
-
if ((child.getAttribute("type") || "line") === "line") text += "\n";
|
|
1969
|
-
break;
|
|
1970
|
-
case "lineBreak":
|
|
1971
|
-
text += "\n";
|
|
1972
|
-
break;
|
|
1973
|
-
// 강제 줄바꿈 — ref 추출기·소스맵 스캐너와 동일 모델
|
|
1974
|
-
case "fwSpace":
|
|
1975
|
-
case "hwSpace":
|
|
1976
|
-
text += " ";
|
|
1977
|
-
break;
|
|
1978
|
-
case "tbl":
|
|
1979
|
-
break;
|
|
1980
|
-
// 테이블은 walkSection에서 처리
|
|
1981
|
-
// 하이퍼링크
|
|
1982
|
-
case "hyperlink": {
|
|
1983
|
-
const url = child.getAttribute("url") || child.getAttribute("href") || "";
|
|
1984
|
-
if (url) {
|
|
1985
|
-
const safe = sanitizeHref(url);
|
|
1986
|
-
if (safe) href = safe;
|
|
1987
|
-
}
|
|
1988
|
-
walk(child);
|
|
1989
|
-
break;
|
|
1990
|
-
}
|
|
1991
|
-
// 각주/미주
|
|
1992
|
-
case "footNote":
|
|
1993
|
-
case "endNote":
|
|
1994
|
-
case "fn":
|
|
1995
|
-
case "en": {
|
|
1996
|
-
const noteText = extractTextFromNode(child);
|
|
1997
|
-
if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
|
|
1998
|
-
break;
|
|
1999
|
-
}
|
|
2000
|
-
// 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
|
|
2001
|
-
case "ctrl":
|
|
2002
|
-
handleCtrl(child);
|
|
2003
|
-
break;
|
|
2004
|
-
// run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
|
|
2005
|
-
case "fieldBegin": {
|
|
2006
|
-
const url = extractHyperlinkHref(child);
|
|
2007
|
-
if (url && !href) href = url;
|
|
2008
|
-
break;
|
|
2009
|
-
}
|
|
2010
|
-
// run 직계 변경추적 마커 (비표준 경로)
|
|
2011
|
-
case "deleteBegin":
|
|
2012
|
-
if (ctx) ctx.shared.track.deleteDepth++;
|
|
2013
|
-
break;
|
|
2014
|
-
case "deleteEnd":
|
|
2015
|
-
if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
|
|
2016
|
-
break;
|
|
2017
|
-
case "insertBegin":
|
|
2018
|
-
case "insertEnd":
|
|
2019
|
-
break;
|
|
2020
|
-
case "fieldEnd":
|
|
2021
|
-
case "parameters":
|
|
2022
|
-
case "stringParam":
|
|
2023
|
-
case "integerParam":
|
|
2024
|
-
case "boolParam":
|
|
2025
|
-
case "floatParam":
|
|
2026
|
-
case "secPr":
|
|
2027
|
-
// 섹션 속성 (페이지 설정 등)
|
|
2028
|
-
case "colPr":
|
|
2029
|
-
// 다단 속성
|
|
2030
|
-
case "linesegarray":
|
|
2031
|
-
case "lineseg":
|
|
2032
|
-
// 레이아웃 정보
|
|
2033
|
-
// 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
|
|
2034
|
-
case "pic":
|
|
2035
|
-
case "shape":
|
|
2036
|
-
case "drawingObject":
|
|
2037
|
-
case "shapeComment":
|
|
2038
|
-
case "drawText":
|
|
2039
|
-
break;
|
|
2040
|
-
// 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
|
|
2041
|
-
// 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
|
|
2042
|
-
// 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
|
|
2043
|
-
case "equation": {
|
|
2044
|
-
const script = findChildByLocalName(child, "script");
|
|
2045
|
-
const raw = script ? extractTextFromNode(script) : "";
|
|
2046
|
-
if (raw.trim()) {
|
|
2047
|
-
try {
|
|
2048
|
-
const latex = hmlToLatex(raw).trim();
|
|
2049
|
-
if (latex) text += " $" + latex + "$ ";
|
|
2050
|
-
} catch {
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
break;
|
|
2054
|
-
}
|
|
2055
|
-
// run 요소에서 charPrIDRef 추출
|
|
2056
|
-
case "r": {
|
|
2057
|
-
const runCharPr = child.getAttribute("charPrIDRef");
|
|
2058
|
-
if (runCharPr && !charPrId) charPrId = runCharPr;
|
|
2059
|
-
walk(child);
|
|
2060
|
-
break;
|
|
2061
|
-
}
|
|
2062
|
-
default:
|
|
2063
|
-
walk(child);
|
|
2064
|
-
break;
|
|
2020
|
+
if (ordered.length > 0) return ordered;
|
|
2021
|
+
}
|
|
2022
|
+
return Array.from(idToHref.values()).sort(compareSectionPaths);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/hwpx/metadata.ts
|
|
2026
|
+
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
2027
|
+
try {
|
|
2028
|
+
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
2029
|
+
for (const mp of metaPaths) {
|
|
2030
|
+
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
2031
|
+
if (!file) continue;
|
|
2032
|
+
const xml = await file.async("text");
|
|
2033
|
+
if (decompressed) {
|
|
2034
|
+
decompressed.total += xml.length * 2;
|
|
2035
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
2065
2036
|
}
|
|
2037
|
+
parseDublinCoreMetadata(xml, metadata);
|
|
2038
|
+
if (metadata.title || metadata.author) return;
|
|
2066
2039
|
}
|
|
2067
|
-
}
|
|
2068
|
-
walk(para);
|
|
2069
|
-
const leaderIdx = text.indexOf("");
|
|
2070
|
-
if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
|
|
2071
|
-
let cleanText = text.replace(/[ \t]+/g, " ").trim();
|
|
2072
|
-
if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
|
|
2073
|
-
cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
|
|
2074
|
-
cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
|
|
2075
|
-
let style;
|
|
2076
|
-
if (styleMap && charPrId) {
|
|
2077
|
-
const charProp = styleMap.charProperties.get(charPrId);
|
|
2078
|
-
if (charProp) {
|
|
2079
|
-
style = {};
|
|
2080
|
-
if (charProp.fontSize) style.fontSize = charProp.fontSize;
|
|
2081
|
-
if (charProp.bold) style.bold = true;
|
|
2082
|
-
if (charProp.italic) style.italic = true;
|
|
2083
|
-
if (charProp.fontName) style.fontName = charProp.fontName;
|
|
2084
|
-
if (!style.fontSize && !style.bold && !style.italic) style = void 0;
|
|
2085
|
-
}
|
|
2040
|
+
} catch {
|
|
2086
2041
|
}
|
|
2087
|
-
return { text: cleanText, href, footnote, style };
|
|
2088
2042
|
}
|
|
2089
|
-
function
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2043
|
+
function parseDublinCoreMetadata(xml, metadata) {
|
|
2044
|
+
const parser = createXmlParser();
|
|
2045
|
+
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
2046
|
+
if (!doc.documentElement) return;
|
|
2047
|
+
const getText = (tagNames) => {
|
|
2048
|
+
for (const tag of tagNames) {
|
|
2049
|
+
const els = doc.getElementsByTagName(tag);
|
|
2050
|
+
if (els.length > 0) {
|
|
2051
|
+
const text = els[0].textContent?.trim();
|
|
2052
|
+
if (text) return text;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return void 0;
|
|
2056
|
+
};
|
|
2057
|
+
metadata.title = metadata.title || getText(["dc:title", "title"]);
|
|
2058
|
+
metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
|
|
2059
|
+
metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
|
|
2060
|
+
metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
|
|
2061
|
+
metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
|
|
2062
|
+
const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
|
|
2063
|
+
if (keywords && !metadata.keywords) {
|
|
2064
|
+
metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
2097
2065
|
}
|
|
2098
|
-
return null;
|
|
2099
2066
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2067
|
+
|
|
2068
|
+
// src/hwpx/parser.ts
|
|
2069
|
+
async function parseHwpxDocument(buffer, options) {
|
|
2070
|
+
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
|
|
2071
|
+
let zip;
|
|
2072
|
+
try {
|
|
2073
|
+
zip = await JSZip3.loadAsync(buffer);
|
|
2074
|
+
} catch {
|
|
2075
|
+
return extractFromBrokenZip(buffer);
|
|
2108
2076
|
}
|
|
2109
|
-
|
|
2077
|
+
const actualEntryCount = Object.keys(zip.files).length;
|
|
2078
|
+
if (actualEntryCount > MAX_ZIP_ENTRIES) {
|
|
2079
|
+
throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
2080
|
+
}
|
|
2081
|
+
const manifestFile = zip.file("META-INF/manifest.xml");
|
|
2082
|
+
if (manifestFile) {
|
|
2083
|
+
const manifestXml = await manifestFile.async("text");
|
|
2084
|
+
if (isEncryptedHwpx(manifestXml)) {
|
|
2085
|
+
if (isComFallbackAvailable() && options?.filePath) {
|
|
2086
|
+
const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
|
|
2087
|
+
if (pages.some((p) => p && p.trim().length > 0)) {
|
|
2088
|
+
return comResultToParseResult(pages, pageCount, warnings2);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
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.");
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const decompressed = { total: 0 };
|
|
2095
|
+
const metadata = {};
|
|
2096
|
+
await extractHwpxMetadata(zip, metadata, decompressed);
|
|
2097
|
+
const styleMap = await extractHwpxStyles(zip, decompressed);
|
|
2098
|
+
const warnings = [];
|
|
2099
|
+
const sectionPaths = await resolveSectionPaths(zip);
|
|
2100
|
+
if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
2101
|
+
metadata.pageCount = sectionPaths.length;
|
|
2102
|
+
const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
|
|
2103
|
+
const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
|
|
2104
|
+
const blocks = [];
|
|
2105
|
+
const shared = createSectionShared();
|
|
2106
|
+
let parsedSections = 0;
|
|
2107
|
+
for (let si = 0; si < sectionPaths.length; si++) {
|
|
2108
|
+
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
2109
|
+
const file = zip.file(sectionPaths[si]);
|
|
2110
|
+
if (!file) continue;
|
|
2111
|
+
try {
|
|
2112
|
+
const xml = await file.async("text");
|
|
2113
|
+
decompressed.total += xml.length * 2;
|
|
2114
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
2115
|
+
blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
|
|
2116
|
+
parsedSections++;
|
|
2117
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
2118
|
+
} catch (secErr) {
|
|
2119
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
2120
|
+
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" });
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
applyPageText(blocks, shared);
|
|
2124
|
+
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
2125
|
+
detectHwpxHeadings(blocks, styleMap);
|
|
2126
|
+
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
2127
|
+
const markdown = blocksToMarkdown(blocks);
|
|
2128
|
+
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 };
|
|
2110
2129
|
}
|
|
2111
2130
|
|
|
2112
2131
|
// src/hwp5/record.ts
|
|
@@ -16748,7 +16767,7 @@ function isDistributionSentinel(markdown) {
|
|
|
16748
16767
|
}
|
|
16749
16768
|
|
|
16750
16769
|
// src/xlsx/parser.ts
|
|
16751
|
-
import
|
|
16770
|
+
import JSZip4 from "jszip";
|
|
16752
16771
|
import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
|
|
16753
16772
|
var MAX_SHEETS = 100;
|
|
16754
16773
|
var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
|
|
@@ -16939,7 +16958,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
|
|
|
16939
16958
|
}
|
|
16940
16959
|
async function parseXlsxDocument(buffer, options) {
|
|
16941
16960
|
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
|
|
16942
|
-
const zip = await
|
|
16961
|
+
const zip = await JSZip4.loadAsync(buffer);
|
|
16943
16962
|
const warnings = [];
|
|
16944
16963
|
const workbookFile = zip.file("xl/workbook.xml");
|
|
16945
16964
|
if (!workbookFile) {
|
|
@@ -17645,7 +17664,7 @@ async function parseXlsDocument(buffer, options) {
|
|
|
17645
17664
|
}
|
|
17646
17665
|
|
|
17647
17666
|
// src/docx/parser.ts
|
|
17648
|
-
import
|
|
17667
|
+
import JSZip5 from "jszip";
|
|
17649
17668
|
import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
|
|
17650
17669
|
|
|
17651
17670
|
// src/docx/equation.ts
|
|
@@ -18399,7 +18418,7 @@ async function extractImages(zip, rels, doc, warnings) {
|
|
|
18399
18418
|
}
|
|
18400
18419
|
async function parseDocxDocument(buffer, options) {
|
|
18401
18420
|
precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
|
|
18402
|
-
const zip = await
|
|
18421
|
+
const zip = await JSZip5.loadAsync(buffer);
|
|
18403
18422
|
const warnings = [];
|
|
18404
18423
|
const docFile = zip.file("word/document.xml");
|
|
18405
18424
|
if (!docFile) {
|
|
@@ -19317,7 +19336,7 @@ function fillInlineFields(text, values, filled, matchedLabels) {
|
|
|
19317
19336
|
}
|
|
19318
19337
|
|
|
19319
19338
|
// src/form/filler-hwpx.ts
|
|
19320
|
-
import
|
|
19339
|
+
import JSZip6 from "jszip";
|
|
19321
19340
|
|
|
19322
19341
|
// src/roundtrip/source-map.ts
|
|
19323
19342
|
function escapeXmlText(text) {
|
|
@@ -19991,7 +20010,7 @@ function patchZipEntries(original, replacements) {
|
|
|
19991
20010
|
// src/form/filler-hwpx.ts
|
|
19992
20011
|
async function fillHwpx(hwpxBuffer, values) {
|
|
19993
20012
|
const u8 = new Uint8Array(hwpxBuffer);
|
|
19994
|
-
const zip = await
|
|
20013
|
+
const zip = await JSZip6.loadAsync(hwpxBuffer);
|
|
19995
20014
|
const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
|
|
19996
20015
|
if (sectionPaths.length === 0) {
|
|
19997
20016
|
throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
@@ -20232,7 +20251,7 @@ async function fillHwpx(hwpxBuffer, values) {
|
|
|
20232
20251
|
}
|
|
20233
20252
|
|
|
20234
20253
|
// src/hwpx/generator.ts
|
|
20235
|
-
import
|
|
20254
|
+
import JSZip7 from "jszip";
|
|
20236
20255
|
|
|
20237
20256
|
// src/hwpx/text-metrics.ts
|
|
20238
20257
|
var ASCII_W = [
|
|
@@ -20603,53 +20622,112 @@ function mmToHwpunit(mm) {
|
|
|
20603
20622
|
return Math.round(mm * 7200 / 25.4);
|
|
20604
20623
|
}
|
|
20605
20624
|
|
|
20606
|
-
// src/
|
|
20607
|
-
|
|
20608
|
-
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
|
|
20612
|
-
|
|
20625
|
+
// src/hwpx/gen-ids.ts
|
|
20626
|
+
var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
|
|
20627
|
+
var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
|
|
20628
|
+
var NS_HEAD = "http://www.hancom.co.kr/hwpml/2011/head";
|
|
20629
|
+
var NS_CORE = "http://www.hancom.co.kr/hwpml/2011/core";
|
|
20630
|
+
var NS_OPF = "http://www.idpf.org/2007/opf/";
|
|
20631
|
+
var NS_HPF = "http://www.hancom.co.kr/schema/2011/hpf";
|
|
20632
|
+
var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
|
|
20633
|
+
var CHAR_NORMAL = 0;
|
|
20634
|
+
var CHAR_BOLD = 1;
|
|
20635
|
+
var CHAR_ITALIC = 2;
|
|
20636
|
+
var CHAR_BOLD_ITALIC = 3;
|
|
20637
|
+
var CHAR_CODE = 4;
|
|
20638
|
+
var CHAR_H1 = 5;
|
|
20639
|
+
var CHAR_H2 = 6;
|
|
20640
|
+
var CHAR_H3 = 7;
|
|
20641
|
+
var CHAR_H4 = 8;
|
|
20642
|
+
var CHAR_TABLE_HEADER = 9;
|
|
20643
|
+
var CHAR_QUOTE = 10;
|
|
20644
|
+
var PARA_NORMAL = 0;
|
|
20645
|
+
var PARA_H1 = 1;
|
|
20646
|
+
var PARA_H2 = 2;
|
|
20647
|
+
var PARA_H3 = 3;
|
|
20648
|
+
var PARA_H4 = 4;
|
|
20649
|
+
var PARA_CODE = 5;
|
|
20650
|
+
var PARA_QUOTE = 6;
|
|
20651
|
+
var PARA_LIST = 7;
|
|
20652
|
+
var DEFAULT_TEXT_COLOR = "#000000";
|
|
20653
|
+
function resolveTheme(theme) {
|
|
20654
|
+
return {
|
|
20655
|
+
h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
|
|
20656
|
+
h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
|
|
20657
|
+
h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20658
|
+
h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
20659
|
+
body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20660
|
+
quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
|
|
20661
|
+
/** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
|
|
20662
|
+
hasQuoteOption: theme?.quoteColor !== void 0,
|
|
20663
|
+
tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
20664
|
+
tableHeaderBold: !!theme?.tableHeaderBold
|
|
20665
|
+
};
|
|
20613
20666
|
}
|
|
20614
|
-
function
|
|
20615
|
-
return
|
|
20667
|
+
function escapeXml(text) {
|
|
20668
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20616
20669
|
}
|
|
20617
|
-
function
|
|
20618
|
-
|
|
20670
|
+
function headingParaPrId(level) {
|
|
20671
|
+
if (level === 1) return PARA_H1;
|
|
20672
|
+
if (level === 2) return PARA_H2;
|
|
20673
|
+
if (level === 3) return PARA_H3;
|
|
20674
|
+
return PARA_H4;
|
|
20619
20675
|
}
|
|
20620
|
-
|
|
20621
|
-
|
|
20622
|
-
if (
|
|
20623
|
-
|
|
20624
|
-
|
|
20625
|
-
|
|
20626
|
-
|
|
20627
|
-
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
|
|
20631
|
-
|
|
20632
|
-
|
|
20633
|
-
|
|
20634
|
-
|
|
20635
|
-
|
|
20636
|
-
|
|
20637
|
-
|
|
20638
|
-
|
|
20639
|
-
|
|
20640
|
-
|
|
20641
|
-
|
|
20642
|
-
|
|
20643
|
-
|
|
20644
|
-
|
|
20676
|
+
function headingCharPrId(level) {
|
|
20677
|
+
if (level === 1) return CHAR_H1;
|
|
20678
|
+
if (level === 2) return CHAR_H2;
|
|
20679
|
+
if (level === 3) return CHAR_H3;
|
|
20680
|
+
return CHAR_H4;
|
|
20681
|
+
}
|
|
20682
|
+
function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_COLOR, ratioPct = 100) {
|
|
20683
|
+
const boldAttr = bold ? ` bold="1"` : "";
|
|
20684
|
+
const italicAttr = italic ? ` italic="1"` : "";
|
|
20685
|
+
const effFont = bold ? 2 : fontId;
|
|
20686
|
+
return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
|
|
20687
|
+
<hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
|
|
20688
|
+
<hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
|
|
20689
|
+
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
20690
|
+
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
20691
|
+
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
20692
|
+
</hh:charPr>`;
|
|
20693
|
+
}
|
|
20694
|
+
function paraPr(id, opts = {}) {
|
|
20695
|
+
const { align = "JUSTIFY", spaceBefore = 0, spaceAfter = 0, lineSpacing = 160, indent = 0, left = 0, keepWord = false } = opts;
|
|
20696
|
+
const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
|
|
20697
|
+
const snapGrid = keepWord ? "0" : "1";
|
|
20698
|
+
return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
|
|
20699
|
+
<hh:align horizontal="${align}" vertical="BASELINE"/>
|
|
20700
|
+
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
20701
|
+
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
20702
|
+
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
20703
|
+
<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>
|
|
20704
|
+
<hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
|
|
20705
|
+
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
20706
|
+
</hh:paraPr>`;
|
|
20707
|
+
}
|
|
20708
|
+
var GONGMUN_LIST_BASE = 8;
|
|
20709
|
+
var GONGMUN_LIST_LEVELS = 8;
|
|
20710
|
+
var GONGMUN_CENTER = GONGMUN_LIST_BASE + GONGMUN_LIST_LEVELS;
|
|
20711
|
+
var CHAR_VARIANT_BASE = 11;
|
|
20712
|
+
var GONGMUN_BODY_RATIO = 95;
|
|
20713
|
+
|
|
20714
|
+
// src/hwpx/md-runs.ts
|
|
20715
|
+
function buildPrvText(blocks) {
|
|
20716
|
+
const lines = [];
|
|
20717
|
+
let bytes = 0;
|
|
20718
|
+
for (const b of blocks) {
|
|
20719
|
+
let text = b.text || (b.rows ? b.rows.map((r) => r.join(" ")).join("\n") : "");
|
|
20720
|
+
if (b.type === "html_table") text = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
20721
|
+
if (!text) continue;
|
|
20722
|
+
lines.push(text);
|
|
20723
|
+
bytes += text.length * 3;
|
|
20724
|
+
if (bytes > 1024) break;
|
|
20645
20725
|
}
|
|
20646
|
-
return
|
|
20726
|
+
return lines.join("\n").slice(0, 1024);
|
|
20647
20727
|
}
|
|
20648
|
-
|
|
20649
|
-
// src/roundtrip/markdown-units.ts
|
|
20650
|
-
function splitMarkdownUnits(md2) {
|
|
20728
|
+
function parseMarkdownToBlocks(md2) {
|
|
20651
20729
|
const lines = md2.split("\n");
|
|
20652
|
-
const
|
|
20730
|
+
const blocks = [];
|
|
20653
20731
|
let i = 0;
|
|
20654
20732
|
while (i < lines.length) {
|
|
20655
20733
|
const line = lines[i];
|
|
@@ -20657,442 +20735,451 @@ function splitMarkdownUnits(md2) {
|
|
|
20657
20735
|
i++;
|
|
20658
20736
|
continue;
|
|
20659
20737
|
}
|
|
20660
|
-
|
|
20661
|
-
|
|
20738
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(.*)$/);
|
|
20739
|
+
if (fenceMatch) {
|
|
20740
|
+
const fence = fenceMatch[1];
|
|
20741
|
+
const lang = fenceMatch[2].trim();
|
|
20742
|
+
const codeLines = [];
|
|
20743
|
+
i++;
|
|
20744
|
+
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
20745
|
+
codeLines.push(lines[i]);
|
|
20746
|
+
i++;
|
|
20747
|
+
}
|
|
20748
|
+
if (i < lines.length) i++;
|
|
20749
|
+
blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
|
|
20750
|
+
continue;
|
|
20751
|
+
}
|
|
20752
|
+
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
20753
|
+
blocks.push({ type: "hr" });
|
|
20754
|
+
i++;
|
|
20755
|
+
continue;
|
|
20756
|
+
}
|
|
20757
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
20758
|
+
if (headingMatch) {
|
|
20759
|
+
blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
|
|
20760
|
+
i++;
|
|
20761
|
+
continue;
|
|
20762
|
+
}
|
|
20763
|
+
if (/^<table[\s>]/i.test(line.trimStart())) {
|
|
20764
|
+
const htmlLines = [];
|
|
20662
20765
|
let depth = 0;
|
|
20663
20766
|
while (i < lines.length) {
|
|
20664
20767
|
const l = lines[i];
|
|
20665
|
-
|
|
20666
|
-
depth += (l.match(/<table
|
|
20667
|
-
depth -= (l.match(/<\/table>/
|
|
20768
|
+
htmlLines.push(l);
|
|
20769
|
+
depth += (l.match(/<table[\s>]/gi) ?? []).length;
|
|
20770
|
+
depth -= (l.match(/<\/table>/gi) ?? []).length;
|
|
20668
20771
|
i++;
|
|
20669
20772
|
if (depth <= 0) break;
|
|
20670
20773
|
}
|
|
20671
|
-
|
|
20774
|
+
blocks.push({ type: "html_table", text: htmlLines.join("\n") });
|
|
20672
20775
|
continue;
|
|
20673
20776
|
}
|
|
20674
20777
|
if (line.trimStart().startsWith("|")) {
|
|
20675
|
-
const
|
|
20778
|
+
const tableRows = [];
|
|
20676
20779
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
20677
|
-
|
|
20780
|
+
const row = lines[i];
|
|
20781
|
+
if (/^[\s|:\-]+$/.test(row)) {
|
|
20782
|
+
i++;
|
|
20783
|
+
continue;
|
|
20784
|
+
}
|
|
20785
|
+
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
20786
|
+
if (cells.length > 0) tableRows.push(cells);
|
|
20678
20787
|
i++;
|
|
20679
20788
|
}
|
|
20680
|
-
|
|
20789
|
+
if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
|
|
20681
20790
|
continue;
|
|
20682
20791
|
}
|
|
20683
|
-
if (
|
|
20684
|
-
|
|
20685
|
-
i
|
|
20792
|
+
if (line.trimStart().startsWith("> ")) {
|
|
20793
|
+
const quoteLines = [];
|
|
20794
|
+
while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
|
|
20795
|
+
quoteLines.push(lines[i].replace(/^>\s?/, ""));
|
|
20796
|
+
i++;
|
|
20797
|
+
}
|
|
20798
|
+
for (const ql of quoteLines) {
|
|
20799
|
+
blocks.push({ type: "blockquote", text: ql.trim() || "" });
|
|
20800
|
+
}
|
|
20686
20801
|
continue;
|
|
20687
20802
|
}
|
|
20688
|
-
|
|
20689
|
-
|
|
20803
|
+
const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
|
|
20804
|
+
if (listMatch) {
|
|
20805
|
+
const indent = Math.floor(listMatch[1].length / 2);
|
|
20806
|
+
const ordered = /\d/.test(listMatch[2]);
|
|
20807
|
+
blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
|
|
20690
20808
|
i++;
|
|
20691
20809
|
continue;
|
|
20692
20810
|
}
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
collected.push(lines[i].trim());
|
|
20696
|
-
i++;
|
|
20697
|
-
}
|
|
20698
|
-
units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
|
|
20811
|
+
blocks.push({ type: "paragraph", text: line.trim() });
|
|
20812
|
+
i++;
|
|
20699
20813
|
}
|
|
20700
|
-
return
|
|
20814
|
+
return blocks;
|
|
20701
20815
|
}
|
|
20702
|
-
function
|
|
20703
|
-
|
|
20704
|
-
|
|
20705
|
-
|
|
20706
|
-
|
|
20707
|
-
|
|
20708
|
-
|
|
20709
|
-
|
|
20816
|
+
function parseInlineMarkdown(text) {
|
|
20817
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
20818
|
+
text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
|
|
20819
|
+
text = text.replace(/~~([^~]+)~~/g, "$1");
|
|
20820
|
+
const spans = [];
|
|
20821
|
+
const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
|
|
20822
|
+
let lastIdx = 0;
|
|
20823
|
+
for (const match of text.matchAll(regex)) {
|
|
20824
|
+
const idx = match.index;
|
|
20825
|
+
if (idx > lastIdx) {
|
|
20826
|
+
spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
|
|
20710
20827
|
}
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
if (
|
|
20715
|
-
|
|
20828
|
+
const raw = match[0];
|
|
20829
|
+
if (raw.startsWith("`")) {
|
|
20830
|
+
spans.push({ text: raw.slice(1, -1), bold: false, italic: false, code: true });
|
|
20831
|
+
} else if (raw.startsWith("***") || raw.startsWith("___")) {
|
|
20832
|
+
spans.push({ text: raw.slice(3, -3), bold: true, italic: true, code: false });
|
|
20833
|
+
} else if (raw.startsWith("**") || raw.startsWith("__")) {
|
|
20834
|
+
spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
|
|
20716
20835
|
} else {
|
|
20717
|
-
|
|
20718
|
-
for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
|
|
20719
|
-
}
|
|
20720
|
-
for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
|
|
20721
|
-
return result2;
|
|
20722
|
-
}
|
|
20723
|
-
const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
|
|
20724
|
-
for (let i2 = 1; i2 <= m; i2++) {
|
|
20725
|
-
for (let j2 = 1; j2 <= n; j2++) {
|
|
20726
|
-
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]);
|
|
20727
|
-
}
|
|
20728
|
-
}
|
|
20729
|
-
const matches = [];
|
|
20730
|
-
let i = m, j = n;
|
|
20731
|
-
while (i > 0 && j > 0) {
|
|
20732
|
-
if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
20733
|
-
matches.push([i - 1, j - 1]);
|
|
20734
|
-
i--;
|
|
20735
|
-
j--;
|
|
20736
|
-
} else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
|
|
20737
|
-
else j--;
|
|
20738
|
-
}
|
|
20739
|
-
matches.reverse();
|
|
20740
|
-
const result = [];
|
|
20741
|
-
let ai = 0, bi = 0;
|
|
20742
|
-
const flushGap = (aEnd, bEnd) => {
|
|
20743
|
-
if (aEnd - ai === bEnd - bi) {
|
|
20744
|
-
while (ai < aEnd) result.push([ai++, bi++]);
|
|
20745
|
-
return;
|
|
20746
|
-
}
|
|
20747
|
-
while (ai < aEnd && bi < bEnd) {
|
|
20748
|
-
const sim = normalizedSimilarity(a[ai], b[bi]);
|
|
20749
|
-
if (sim >= 0.4) {
|
|
20750
|
-
if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
|
|
20751
|
-
result.push([ai++, null]);
|
|
20752
|
-
} else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
|
|
20753
|
-
result.push([null, bi++]);
|
|
20754
|
-
} else {
|
|
20755
|
-
result.push([ai++, bi++]);
|
|
20756
|
-
}
|
|
20757
|
-
} else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
|
|
20758
|
-
else result.push([null, bi++]);
|
|
20759
|
-
}
|
|
20760
|
-
while (ai < aEnd) result.push([ai++, null]);
|
|
20761
|
-
while (bi < bEnd) result.push([null, bi++]);
|
|
20762
|
-
};
|
|
20763
|
-
for (const [pi, pj] of matches) {
|
|
20764
|
-
flushGap(pi, pj);
|
|
20765
|
-
result.push([ai++, bi++]);
|
|
20766
|
-
}
|
|
20767
|
-
flushGap(m, n);
|
|
20768
|
-
return result;
|
|
20769
|
-
}
|
|
20770
|
-
function bestSimInRange(arr, from, to, target) {
|
|
20771
|
-
let best = 0;
|
|
20772
|
-
for (let k = from; k <= to && k < arr.length; k++) {
|
|
20773
|
-
const s = normalizedSimilarity(arr[k], target);
|
|
20774
|
-
if (s > best) best = s;
|
|
20775
|
-
}
|
|
20776
|
-
return best;
|
|
20777
|
-
}
|
|
20778
|
-
function escapeGfm(text) {
|
|
20779
|
-
return text.replace(/~/g, "\\~");
|
|
20780
|
-
}
|
|
20781
|
-
var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
|
|
20782
|
-
function sanitizeText(text) {
|
|
20783
|
-
let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
|
|
20784
|
-
if (result.length <= 30 && result.includes(" ")) {
|
|
20785
|
-
const tokens = result.split(" ");
|
|
20786
|
-
const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-ㄱ-ㆎ]/.test(t)).length;
|
|
20787
|
-
if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
|
|
20788
|
-
result = tokens.join("");
|
|
20836
|
+
spans.push({ text: raw.slice(1, -1), bold: false, italic: true, code: false });
|
|
20789
20837
|
}
|
|
20838
|
+
lastIdx = idx + raw.length;
|
|
20790
20839
|
}
|
|
20791
|
-
|
|
20792
|
-
}
|
|
20793
|
-
function normForMatch(text) {
|
|
20794
|
-
return sanitizeText(text).replace(/\s+/g, " ").trim();
|
|
20795
|
-
}
|
|
20796
|
-
function unescapeGfm(text) {
|
|
20797
|
-
return text.replace(/\\~/g, "~");
|
|
20798
|
-
}
|
|
20799
|
-
function summarize(text) {
|
|
20800
|
-
const t = text.replace(/\s+/g, " ").trim();
|
|
20801
|
-
return t.length > 80 ? t.slice(0, 77) + "..." : t;
|
|
20802
|
-
}
|
|
20803
|
-
function replicateGfmTable(table) {
|
|
20804
|
-
const { cells, rows: numRows, cols: numCols } = table;
|
|
20805
|
-
if (numRows === 0 || numCols === 0) return null;
|
|
20806
|
-
if (numRows === 1 && numCols === 1) return null;
|
|
20807
|
-
if (numCols === 1) return null;
|
|
20808
|
-
const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
|
|
20809
|
-
const skip = /* @__PURE__ */ new Set();
|
|
20810
|
-
for (let r = 0; r < numRows; r++) {
|
|
20811
|
-
for (let c = 0; c < numCols; c++) {
|
|
20812
|
-
if (skip.has(`${r},${c}`)) continue;
|
|
20813
|
-
const cell = cells[r]?.[c];
|
|
20814
|
-
if (!cell) continue;
|
|
20815
|
-
display[r][c] = {
|
|
20816
|
-
text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
|
|
20817
|
-
gridR: r,
|
|
20818
|
-
gridC: c
|
|
20819
|
-
};
|
|
20820
|
-
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
20821
|
-
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
20822
|
-
if (dr === 0 && dc === 0) continue;
|
|
20823
|
-
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
20824
|
-
}
|
|
20825
|
-
}
|
|
20826
|
-
c += cell.colSpan - 1;
|
|
20827
|
-
}
|
|
20840
|
+
if (lastIdx < text.length) {
|
|
20841
|
+
spans.push({ text: text.slice(lastIdx), bold: false, italic: false, code: false });
|
|
20828
20842
|
}
|
|
20829
|
-
|
|
20830
|
-
|
|
20831
|
-
for (let r = 0; r < display.length; r++) {
|
|
20832
|
-
const row = display[r];
|
|
20833
|
-
if (row.every((cell) => cell.text === "")) continue;
|
|
20834
|
-
const nonEmptyCols = row.filter((cell) => cell.text !== "");
|
|
20835
|
-
const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
|
|
20836
|
-
if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
|
|
20837
|
-
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
20838
|
-
pendingLabelRow = row;
|
|
20839
|
-
continue;
|
|
20840
|
-
}
|
|
20841
|
-
if (pendingLabelRow) {
|
|
20842
|
-
if (row[0].text === "") row[0] = pendingLabelRow[0];
|
|
20843
|
-
else uniqueRows.push(pendingLabelRow);
|
|
20844
|
-
pendingLabelRow = null;
|
|
20845
|
-
}
|
|
20846
|
-
uniqueRows.push(row);
|
|
20843
|
+
if (spans.length === 0) {
|
|
20844
|
+
spans.push({ text, bold: false, italic: false, code: false });
|
|
20847
20845
|
}
|
|
20848
|
-
|
|
20849
|
-
return uniqueRows.length > 0 ? uniqueRows : null;
|
|
20846
|
+
return spans;
|
|
20850
20847
|
}
|
|
20851
|
-
function
|
|
20852
|
-
|
|
20853
|
-
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
if (cells.length === 0) continue;
|
|
20858
|
-
if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
|
20859
|
-
rows.push(cells);
|
|
20860
|
-
}
|
|
20861
|
-
return rows;
|
|
20848
|
+
function spanToCharPrId(span) {
|
|
20849
|
+
if (span.code) return CHAR_CODE;
|
|
20850
|
+
if (span.bold && span.italic) return CHAR_BOLD_ITALIC;
|
|
20851
|
+
if (span.bold) return CHAR_BOLD;
|
|
20852
|
+
if (span.italic) return CHAR_ITALIC;
|
|
20853
|
+
return CHAR_NORMAL;
|
|
20862
20854
|
}
|
|
20863
|
-
function
|
|
20864
|
-
|
|
20855
|
+
function generateRuns(text, defaultCharPr = CHAR_NORMAL, mapCharId) {
|
|
20856
|
+
const spans = parseInlineMarkdown(text);
|
|
20857
|
+
return spans.map((span) => {
|
|
20858
|
+
let charId = span.code || span.bold || span.italic ? spanToCharPrId(span) : defaultCharPr;
|
|
20859
|
+
if (mapCharId) charId = mapCharId(charId);
|
|
20860
|
+
return `<hp:run charPrIDRef="${charId}"><hp:t>${escapeXml(span.text)}</hp:t></hp:run>`;
|
|
20861
|
+
}).join("");
|
|
20865
20862
|
}
|
|
20866
|
-
function
|
|
20867
|
-
if (
|
|
20868
|
-
return
|
|
20869
|
-
if (b.type === "table" && b.table) {
|
|
20870
|
-
const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
|
|
20871
|
-
return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
|
|
20872
|
-
}
|
|
20873
|
-
if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
|
|
20874
|
-
const t = sanitizeText(b.text ?? "");
|
|
20875
|
-
return t ? t.replace(/\n/g, "<br>") : "";
|
|
20876
|
-
}).filter(Boolean).join("<br>");
|
|
20863
|
+
function generateParagraph(text, paraPrId = PARA_NORMAL, charPrId = CHAR_NORMAL, mapCharId) {
|
|
20864
|
+
if (paraPrId === PARA_CODE) {
|
|
20865
|
+
return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_CODE}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
|
|
20877
20866
|
}
|
|
20878
|
-
|
|
20867
|
+
const runs = generateRuns(text, charPrId, mapCharId);
|
|
20868
|
+
return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
|
|
20879
20869
|
}
|
|
20880
|
-
|
|
20881
|
-
|
|
20882
|
-
|
|
20883
|
-
|
|
20884
|
-
|
|
20885
|
-
|
|
20886
|
-
|
|
20887
|
-
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20892
|
-
|
|
20870
|
+
|
|
20871
|
+
// src/hwpx/gen-header.ts
|
|
20872
|
+
function generateContainerXml() {
|
|
20873
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20874
|
+
<ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
|
|
20875
|
+
<ocf:rootfiles>
|
|
20876
|
+
<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
|
|
20877
|
+
</ocf:rootfiles>
|
|
20878
|
+
</ocf:container>`;
|
|
20879
|
+
}
|
|
20880
|
+
function generateManifest() {
|
|
20881
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20882
|
+
<opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
|
|
20883
|
+
<opf:manifest>
|
|
20884
|
+
<opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
|
|
20885
|
+
<opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>
|
|
20886
|
+
</opf:manifest>
|
|
20887
|
+
<opf:spine>
|
|
20888
|
+
<opf:itemref idref="header" linear="no"/>
|
|
20889
|
+
<opf:itemref idref="section0" linear="yes"/>
|
|
20890
|
+
</opf:spine>
|
|
20891
|
+
</opf:package>`;
|
|
20892
|
+
}
|
|
20893
|
+
function buildCharProperties(theme, gongmun, ratioVariants = []) {
|
|
20894
|
+
let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
|
|
20895
|
+
if (gongmun) {
|
|
20896
|
+
body = gongmun.bodyHeight;
|
|
20897
|
+
code = Math.max(body - 200, 900);
|
|
20898
|
+
h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
|
|
20899
|
+
h2 = 1600;
|
|
20900
|
+
h3 = body;
|
|
20901
|
+
h4 = Math.max(body - 100, 1300);
|
|
20893
20902
|
}
|
|
20894
|
-
|
|
20895
|
-
|
|
20903
|
+
const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
|
|
20904
|
+
const rows = [
|
|
20905
|
+
charPr(0, body, false, false, 0, theme.body, bodyRatio),
|
|
20906
|
+
charPr(1, body, true, false, 0, theme.body, bodyRatio),
|
|
20907
|
+
charPr(2, body, false, true, 0, theme.body, bodyRatio),
|
|
20908
|
+
charPr(3, body, true, true, 0, theme.body, bodyRatio),
|
|
20909
|
+
charPr(4, code, false, false, 1),
|
|
20910
|
+
charPr(5, h1, true, false, 1, theme.h1),
|
|
20911
|
+
charPr(6, h2, true, false, 1, theme.h2),
|
|
20912
|
+
charPr(7, h3, true, false, 1, theme.h3),
|
|
20913
|
+
charPr(8, h4, true, false, 1, theme.h4),
|
|
20914
|
+
charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, 0, theme.tableHeader),
|
|
20915
|
+
charPr(CHAR_QUOTE, body, false, true, 0, theme.quote)
|
|
20916
|
+
];
|
|
20917
|
+
for (const r of ratioVariants) {
|
|
20918
|
+
rows.push(
|
|
20919
|
+
charPr(rows.length, body, false, false, 0, theme.body, r),
|
|
20920
|
+
charPr(rows.length + 1, body, true, false, 0, theme.body, r),
|
|
20921
|
+
charPr(rows.length + 2, body, false, true, 0, theme.body, r),
|
|
20922
|
+
charPr(rows.length + 3, body, true, true, 0, theme.body, r)
|
|
20923
|
+
);
|
|
20924
|
+
}
|
|
20925
|
+
return `<hh:charProperties itemCnt="${rows.length}">
|
|
20926
|
+
${rows.join("\n")}
|
|
20927
|
+
</hh:charProperties>`;
|
|
20896
20928
|
}
|
|
20897
|
-
function
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
20911
|
-
|
|
20912
|
-
}
|
|
20913
|
-
}
|
|
20914
|
-
rowCells.push({
|
|
20915
|
-
inner: replicateCellInnerHtml(cell),
|
|
20916
|
-
colSpan: cell.colSpan,
|
|
20917
|
-
rowSpan: cell.rowSpan,
|
|
20918
|
-
gridR: r,
|
|
20919
|
-
gridC: c
|
|
20920
|
-
});
|
|
20921
|
-
}
|
|
20922
|
-
if (rowCells.length) result.push({ tag, cells: rowCells });
|
|
20929
|
+
function buildParaProperties(gongmun) {
|
|
20930
|
+
if (!gongmun) {
|
|
20931
|
+
const base2 = [
|
|
20932
|
+
paraPr(0),
|
|
20933
|
+
paraPr(1, { align: "LEFT", spaceBefore: 800, spaceAfter: 200, lineSpacing: 180 }),
|
|
20934
|
+
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: 170 }),
|
|
20935
|
+
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: 160 }),
|
|
20936
|
+
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
|
|
20937
|
+
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
|
|
20938
|
+
paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
|
|
20939
|
+
paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
|
|
20940
|
+
];
|
|
20941
|
+
return `<hh:paraProperties itemCnt="${base2.length}">
|
|
20942
|
+
${base2.join("\n")}
|
|
20943
|
+
</hh:paraProperties>`;
|
|
20923
20944
|
}
|
|
20924
|
-
|
|
20945
|
+
const ls = gongmun.lineSpacing;
|
|
20946
|
+
const titleAlign = gongmun.centerTitle ? "CENTER" : "LEFT";
|
|
20947
|
+
const base = [
|
|
20948
|
+
paraPr(0, { lineSpacing: ls, keepWord: true }),
|
|
20949
|
+
paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
|
|
20950
|
+
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
|
|
20951
|
+
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
20952
|
+
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
20953
|
+
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
|
|
20954
|
+
paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
|
|
20955
|
+
paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
|
|
20956
|
+
];
|
|
20957
|
+
for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
|
|
20958
|
+
const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
|
|
20959
|
+
const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
|
|
20960
|
+
base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
|
|
20961
|
+
}
|
|
20962
|
+
base.push(paraPr(GONGMUN_CENTER, { align: "CENTER", lineSpacing: ls, keepWord: true }));
|
|
20963
|
+
return `<hh:paraProperties itemCnt="${base.length}">
|
|
20964
|
+
${base.join("\n")}
|
|
20965
|
+
</hh:paraProperties>`;
|
|
20966
|
+
}
|
|
20967
|
+
function generateHeaderXml(theme, gongmun, ratioVariants = []) {
|
|
20968
|
+
const bodyFace = gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5";
|
|
20969
|
+
const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants);
|
|
20970
|
+
const paraPropsXml = buildParaProperties(gongmun);
|
|
20971
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
20972
|
+
<hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
|
|
20973
|
+
<hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
|
|
20974
|
+
<hh:refList>
|
|
20975
|
+
<hh:fontfaces itemCnt="7">
|
|
20976
|
+
<hh:fontface lang="HANGUL" fontCnt="3">
|
|
20977
|
+
<hh:font id="0" face="${bodyFace}" type="TTF" isEmbedded="0">
|
|
20978
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20979
|
+
</hh:font>
|
|
20980
|
+
<hh:font id="1" face="\uD568\uCD08\uB86C\uB3CB\uC6C0" type="TTF" isEmbedded="0">
|
|
20981
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20982
|
+
</hh:font>
|
|
20983
|
+
<hh:font id="2" face="HY\uACAC\uACE0\uB515" type="TTF" isEmbedded="0">
|
|
20984
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
20985
|
+
</hh:font>
|
|
20986
|
+
</hh:fontface>
|
|
20987
|
+
<hh:fontface lang="LATIN" fontCnt="3">
|
|
20988
|
+
<hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
|
|
20989
|
+
<hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
|
|
20990
|
+
</hh:font>
|
|
20991
|
+
<hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
|
|
20992
|
+
<hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
20993
|
+
</hh:font>
|
|
20994
|
+
<hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
|
|
20995
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
20996
|
+
</hh:font>
|
|
20997
|
+
</hh:fontface>
|
|
20998
|
+
<hh:fontface lang="HANJA" fontCnt="1">
|
|
20999
|
+
<hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
|
|
21000
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21001
|
+
</hh:font>
|
|
21002
|
+
</hh:fontface>
|
|
21003
|
+
<hh:fontface lang="JAPANESE" fontCnt="1">
|
|
21004
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21005
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21006
|
+
</hh:font>
|
|
21007
|
+
</hh:fontface>
|
|
21008
|
+
<hh:fontface lang="OTHER" fontCnt="1">
|
|
21009
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21010
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21011
|
+
</hh:font>
|
|
21012
|
+
</hh:fontface>
|
|
21013
|
+
<hh:fontface lang="SYMBOL" fontCnt="1">
|
|
21014
|
+
<hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
|
|
21015
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21016
|
+
</hh:font>
|
|
21017
|
+
</hh:fontface>
|
|
21018
|
+
<hh:fontface lang="USER" fontCnt="1">
|
|
21019
|
+
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21020
|
+
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21021
|
+
</hh:font>
|
|
21022
|
+
</hh:fontface>
|
|
21023
|
+
</hh:fontfaces>
|
|
21024
|
+
<hh:borderFills itemCnt="2">
|
|
21025
|
+
<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21026
|
+
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21027
|
+
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21028
|
+
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21029
|
+
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21030
|
+
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21031
|
+
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21032
|
+
</hh:borderFill>
|
|
21033
|
+
<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21034
|
+
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21035
|
+
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21036
|
+
<hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21037
|
+
<hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21038
|
+
<hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21039
|
+
<hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21040
|
+
</hh:borderFill>
|
|
21041
|
+
</hh:borderFills>
|
|
21042
|
+
${charPropsXml}
|
|
21043
|
+
<hh:tabProperties itemCnt="0"/>
|
|
21044
|
+
<hh:numberings itemCnt="0"/>
|
|
21045
|
+
<hh:bullets itemCnt="0"/>
|
|
21046
|
+
${paraPropsXml}
|
|
21047
|
+
<hh:styles itemCnt="1">
|
|
21048
|
+
<hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
|
|
21049
|
+
</hh:styles>
|
|
21050
|
+
</hh:refList>
|
|
21051
|
+
<hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
|
|
21052
|
+
</hh:head>`;
|
|
20925
21053
|
}
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
|
|
20929
|
-
|
|
20930
|
-
|
|
20931
|
-
|
|
20932
|
-
const
|
|
20933
|
-
|
|
20934
|
-
|
|
20935
|
-
|
|
20936
|
-
|
|
20937
|
-
|
|
20938
|
-
|
|
20939
|
-
|
|
20940
|
-
|
|
20941
|
-
|
|
20942
|
-
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
20948
|
-
|
|
20949
|
-
|
|
21054
|
+
|
|
21055
|
+
// src/hwpx/gen-gongmun-fit.ts
|
|
21056
|
+
function plainRenderText(text) {
|
|
21057
|
+
return parseInlineMarkdown(text).map((s) => s.text).join("");
|
|
21058
|
+
}
|
|
21059
|
+
function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
|
|
21060
|
+
const minRatio = gongmun.autoFitMinRatio;
|
|
21061
|
+
if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
|
|
21062
|
+
const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
|
|
21063
|
+
const ratioByBlock = /* @__PURE__ */ new Map();
|
|
21064
|
+
const variants = [];
|
|
21065
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
21066
|
+
const block = blocks[i];
|
|
21067
|
+
let text;
|
|
21068
|
+
let firstW;
|
|
21069
|
+
let contW;
|
|
21070
|
+
if (block.type === "list_item" && gongmunList.has(i)) {
|
|
21071
|
+
const { marker, depth } = gongmunList.get(i);
|
|
21072
|
+
const content = plainRenderText(block.text || "");
|
|
21073
|
+
text = marker ? `${marker} ${content}` : content;
|
|
21074
|
+
const { left, indent } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering);
|
|
21075
|
+
firstW = pageW - left - Math.max(indent, 0);
|
|
21076
|
+
contW = pageW - left - Math.max(-indent, 0);
|
|
21077
|
+
} else if (block.type === "paragraph") {
|
|
21078
|
+
const raw = (block.text || "").trim();
|
|
21079
|
+
if (/^<center>[\s\S]*<\/center>$/i.test(raw)) continue;
|
|
21080
|
+
text = plainRenderText(raw);
|
|
21081
|
+
firstW = contW = pageW;
|
|
20950
21082
|
} else {
|
|
20951
|
-
|
|
20952
|
-
const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
20953
|
-
const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
20954
|
-
cellStart = m.index + m[0].length;
|
|
20955
|
-
cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
|
|
20956
|
-
} else if (cellStart >= 0 && cellInfo && currentRow) {
|
|
20957
|
-
currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
|
|
20958
|
-
cellStart = -1;
|
|
20959
|
-
cellInfo = null;
|
|
20960
|
-
}
|
|
21083
|
+
continue;
|
|
20961
21084
|
}
|
|
21085
|
+
if (!text) continue;
|
|
21086
|
+
const r = fitRatioForFewerLines(text, firstW, contW, gongmun.bodyHeight, GONGMUN_BODY_RATIO, minRatio);
|
|
21087
|
+
if (r === null) continue;
|
|
21088
|
+
ratioByBlock.set(i, r);
|
|
21089
|
+
if (!variants.includes(r)) variants.push(r);
|
|
20962
21090
|
}
|
|
20963
|
-
|
|
20964
|
-
return rows;
|
|
21091
|
+
return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
|
|
20965
21092
|
}
|
|
20966
|
-
|
|
20967
|
-
|
|
20968
|
-
|
|
20969
|
-
|
|
20970
|
-
|
|
20971
|
-
hadNonText = true;
|
|
20972
|
-
work = removeNestedTables(work);
|
|
20973
|
-
}
|
|
20974
|
-
if (/<img\s/i.test(work)) {
|
|
20975
|
-
hadNonText = true;
|
|
20976
|
-
work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
|
|
20977
|
-
}
|
|
20978
|
-
const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
20979
|
-
return { lines, hadNonText };
|
|
21093
|
+
function variantMapper(fit, blockIdx) {
|
|
21094
|
+
const r = fit.ratioByBlock.get(blockIdx);
|
|
21095
|
+
if (r === void 0) return void 0;
|
|
21096
|
+
const vi = fit.variants.indexOf(r);
|
|
21097
|
+
return (id) => id >= 0 && id <= 3 ? CHAR_VARIANT_BASE + vi * 4 + id : id;
|
|
20980
21098
|
}
|
|
20981
|
-
function
|
|
20982
|
-
const result =
|
|
20983
|
-
let
|
|
20984
|
-
|
|
20985
|
-
|
|
20986
|
-
|
|
20987
|
-
|
|
20988
|
-
if (m[1] !== "/") {
|
|
20989
|
-
if (depth === 0) start = m.index;
|
|
20990
|
-
depth++;
|
|
20991
|
-
} else {
|
|
20992
|
-
depth--;
|
|
20993
|
-
if (depth === 0 && start >= 0) {
|
|
20994
|
-
result.push(html.slice(start, m.index + m[0].length));
|
|
20995
|
-
start = -1;
|
|
20996
|
-
}
|
|
20997
|
-
if (depth < 0) depth = 0;
|
|
21099
|
+
function precomputeGongmunList(blocks, gongmun) {
|
|
21100
|
+
const result = /* @__PURE__ */ new Map();
|
|
21101
|
+
let i = 0;
|
|
21102
|
+
while (i < blocks.length) {
|
|
21103
|
+
if (blocks[i].type !== "list_item") {
|
|
21104
|
+
i++;
|
|
21105
|
+
continue;
|
|
20998
21106
|
}
|
|
20999
|
-
|
|
21000
|
-
|
|
21001
|
-
|
|
21002
|
-
|
|
21003
|
-
|
|
21004
|
-
|
|
21005
|
-
|
|
21006
|
-
|
|
21007
|
-
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
|
|
21015
|
-
|
|
21107
|
+
const run = [];
|
|
21108
|
+
while (i < blocks.length) {
|
|
21109
|
+
const t = blocks[i].type;
|
|
21110
|
+
if (t === "list_item") {
|
|
21111
|
+
run.push(i);
|
|
21112
|
+
i++;
|
|
21113
|
+
continue;
|
|
21114
|
+
}
|
|
21115
|
+
if (t === "table" || t === "html_table") {
|
|
21116
|
+
let j = i + 1;
|
|
21117
|
+
while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
|
|
21118
|
+
if (j < blocks.length && blocks[j].type === "list_item") {
|
|
21119
|
+
i = j;
|
|
21120
|
+
continue;
|
|
21121
|
+
}
|
|
21122
|
+
}
|
|
21123
|
+
break;
|
|
21016
21124
|
}
|
|
21125
|
+
const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
|
|
21126
|
+
const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
|
|
21127
|
+
const numberer = new GongmunNumberer(gongmun.numbering);
|
|
21128
|
+
run.forEach((bi, k) => {
|
|
21129
|
+
const marker = numberer.next(depths[k], suppress[k]);
|
|
21130
|
+
result.set(bi, { marker, depth: depths[k] });
|
|
21131
|
+
});
|
|
21017
21132
|
}
|
|
21018
|
-
if (depth === 0) result += html.slice(last);
|
|
21019
21133
|
return result;
|
|
21020
21134
|
}
|
|
21021
21135
|
|
|
21022
|
-
// src/
|
|
21023
|
-
|
|
21024
|
-
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
|
|
21028
|
-
|
|
21029
|
-
var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
|
|
21030
|
-
var CHAR_NORMAL = 0;
|
|
21031
|
-
var CHAR_BOLD = 1;
|
|
21032
|
-
var CHAR_ITALIC = 2;
|
|
21033
|
-
var CHAR_BOLD_ITALIC = 3;
|
|
21034
|
-
var CHAR_CODE = 4;
|
|
21035
|
-
var CHAR_H1 = 5;
|
|
21036
|
-
var CHAR_H2 = 6;
|
|
21037
|
-
var CHAR_H3 = 7;
|
|
21038
|
-
var CHAR_H4 = 8;
|
|
21039
|
-
var CHAR_TABLE_HEADER = 9;
|
|
21040
|
-
var CHAR_QUOTE = 10;
|
|
21041
|
-
var PARA_NORMAL = 0;
|
|
21042
|
-
var PARA_H1 = 1;
|
|
21043
|
-
var PARA_H2 = 2;
|
|
21044
|
-
var PARA_H3 = 3;
|
|
21045
|
-
var PARA_H4 = 4;
|
|
21046
|
-
var PARA_CODE = 5;
|
|
21047
|
-
var PARA_QUOTE = 6;
|
|
21048
|
-
var PARA_LIST = 7;
|
|
21049
|
-
var DEFAULT_TEXT_COLOR = "#000000";
|
|
21050
|
-
function resolveTheme(theme) {
|
|
21051
|
-
return {
|
|
21052
|
-
h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
|
|
21053
|
-
h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
|
|
21054
|
-
h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
21055
|
-
h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
|
|
21056
|
-
body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
21057
|
-
quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
|
|
21058
|
-
/** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
|
|
21059
|
-
hasQuoteOption: theme?.quoteColor !== void 0,
|
|
21060
|
-
tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
|
|
21061
|
-
tableHeaderBold: !!theme?.tableHeaderBold
|
|
21062
|
-
};
|
|
21136
|
+
// src/diff/text-diff.ts
|
|
21137
|
+
function similarity(a, b) {
|
|
21138
|
+
if (a === b) return 1;
|
|
21139
|
+
if (!a || !b) return 0;
|
|
21140
|
+
const maxLen = Math.max(a.length, b.length);
|
|
21141
|
+
if (maxLen === 0) return 1;
|
|
21142
|
+
return 1 - levenshtein(a, b) / maxLen;
|
|
21063
21143
|
}
|
|
21064
|
-
|
|
21065
|
-
|
|
21066
|
-
const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
|
|
21067
|
-
const blocks = parseMarkdownToBlocks(markdown);
|
|
21068
|
-
const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
|
|
21069
|
-
const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
|
|
21070
|
-
const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
|
|
21071
|
-
const zip = new JSZip6();
|
|
21072
|
-
zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
|
|
21073
|
-
zip.file("META-INF/container.xml", generateContainerXml());
|
|
21074
|
-
zip.file("Contents/content.hpf", generateManifest());
|
|
21075
|
-
zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
|
|
21076
|
-
zip.file("Contents/section0.xml", sectionXml);
|
|
21077
|
-
zip.file("Preview/PrvText.txt", buildPrvText(blocks));
|
|
21078
|
-
return await zip.generateAsync({ type: "arraybuffer" });
|
|
21144
|
+
function normalizedSimilarity(a, b) {
|
|
21145
|
+
return similarity(normalize(a), normalize(b));
|
|
21079
21146
|
}
|
|
21080
|
-
function
|
|
21081
|
-
|
|
21082
|
-
|
|
21083
|
-
|
|
21084
|
-
|
|
21085
|
-
|
|
21086
|
-
|
|
21087
|
-
|
|
21088
|
-
|
|
21089
|
-
|
|
21147
|
+
function normalize(s) {
|
|
21148
|
+
return s.replace(/\s+/g, " ").trim();
|
|
21149
|
+
}
|
|
21150
|
+
var MAX_LEVENSHTEIN_LEN = 1e4;
|
|
21151
|
+
function levenshtein(a, b) {
|
|
21152
|
+
if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
|
|
21153
|
+
const sampleLen = Math.min(500, a.length, b.length);
|
|
21154
|
+
let diffs = 0;
|
|
21155
|
+
for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
|
|
21156
|
+
const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
|
|
21157
|
+
return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
|
|
21158
|
+
}
|
|
21159
|
+
if (a.length > b.length) [a, b] = [b, a];
|
|
21160
|
+
const m = a.length;
|
|
21161
|
+
const n = b.length;
|
|
21162
|
+
let prev = Array.from({ length: m + 1 }, (_, i) => i);
|
|
21163
|
+
let curr = new Array(m + 1);
|
|
21164
|
+
for (let j = 1; j <= n; j++) {
|
|
21165
|
+
curr[0] = j;
|
|
21166
|
+
for (let i = 1; i <= m; i++) {
|
|
21167
|
+
if (a[i - 1] === b[j - 1]) {
|
|
21168
|
+
curr[i] = prev[i - 1];
|
|
21169
|
+
} else {
|
|
21170
|
+
curr[i] = 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
|
|
21171
|
+
}
|
|
21172
|
+
}
|
|
21173
|
+
;
|
|
21174
|
+
[prev, curr] = [curr, prev];
|
|
21090
21175
|
}
|
|
21091
|
-
return
|
|
21176
|
+
return prev[m];
|
|
21092
21177
|
}
|
|
21093
|
-
|
|
21178
|
+
|
|
21179
|
+
// src/roundtrip/markdown-units.ts
|
|
21180
|
+
function splitMarkdownUnits(md2) {
|
|
21094
21181
|
const lines = md2.split("\n");
|
|
21095
|
-
const
|
|
21182
|
+
const units = [];
|
|
21096
21183
|
let i = 0;
|
|
21097
21184
|
while (i < lines.length) {
|
|
21098
21185
|
const line = lines[i];
|
|
@@ -21100,420 +21187,369 @@ function parseMarkdownToBlocks(md2) {
|
|
|
21100
21187
|
i++;
|
|
21101
21188
|
continue;
|
|
21102
21189
|
}
|
|
21103
|
-
|
|
21104
|
-
|
|
21105
|
-
const fence = fenceMatch[1];
|
|
21106
|
-
const lang = fenceMatch[2].trim();
|
|
21107
|
-
const codeLines = [];
|
|
21108
|
-
i++;
|
|
21109
|
-
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
21110
|
-
codeLines.push(lines[i]);
|
|
21111
|
-
i++;
|
|
21112
|
-
}
|
|
21113
|
-
if (i < lines.length) i++;
|
|
21114
|
-
blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
|
|
21115
|
-
continue;
|
|
21116
|
-
}
|
|
21117
|
-
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
21118
|
-
blocks.push({ type: "hr" });
|
|
21119
|
-
i++;
|
|
21120
|
-
continue;
|
|
21121
|
-
}
|
|
21122
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
21123
|
-
if (headingMatch) {
|
|
21124
|
-
blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
|
|
21125
|
-
i++;
|
|
21126
|
-
continue;
|
|
21127
|
-
}
|
|
21128
|
-
if (/^<table[\s>]/i.test(line.trimStart())) {
|
|
21129
|
-
const htmlLines = [];
|
|
21190
|
+
if (line.trim().startsWith("<table>")) {
|
|
21191
|
+
const collected2 = [];
|
|
21130
21192
|
let depth = 0;
|
|
21131
21193
|
while (i < lines.length) {
|
|
21132
21194
|
const l = lines[i];
|
|
21133
|
-
|
|
21134
|
-
depth += (l.match(/<table
|
|
21135
|
-
depth -= (l.match(/<\/table>/
|
|
21195
|
+
collected2.push(l);
|
|
21196
|
+
depth += (l.match(/<table>/g) || []).length;
|
|
21197
|
+
depth -= (l.match(/<\/table>/g) || []).length;
|
|
21136
21198
|
i++;
|
|
21137
21199
|
if (depth <= 0) break;
|
|
21138
21200
|
}
|
|
21139
|
-
|
|
21201
|
+
units.push({ kind: "html-table", raw: collected2.join("\n"), lines: collected2 });
|
|
21140
21202
|
continue;
|
|
21141
21203
|
}
|
|
21142
21204
|
if (line.trimStart().startsWith("|")) {
|
|
21143
|
-
const
|
|
21205
|
+
const collected2 = [];
|
|
21144
21206
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
21145
|
-
|
|
21146
|
-
if (/^[\s|:\-]+$/.test(row)) {
|
|
21147
|
-
i++;
|
|
21148
|
-
continue;
|
|
21149
|
-
}
|
|
21150
|
-
const cells = row.split("|").slice(1, -1).map((c) => c.trim());
|
|
21151
|
-
if (cells.length > 0) tableRows.push(cells);
|
|
21207
|
+
collected2.push(lines[i]);
|
|
21152
21208
|
i++;
|
|
21153
21209
|
}
|
|
21154
|
-
|
|
21210
|
+
units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
|
|
21155
21211
|
continue;
|
|
21156
21212
|
}
|
|
21157
|
-
if (line.
|
|
21158
|
-
|
|
21159
|
-
|
|
21160
|
-
quoteLines.push(lines[i].replace(/^>\s?/, ""));
|
|
21161
|
-
i++;
|
|
21162
|
-
}
|
|
21163
|
-
for (const ql of quoteLines) {
|
|
21164
|
-
blocks.push({ type: "blockquote", text: ql.trim() || "" });
|
|
21165
|
-
}
|
|
21213
|
+
if (/^-{3,}\s*$/.test(line.trim())) {
|
|
21214
|
+
units.push({ kind: "separator", raw: line.trim(), lines: [line.trim()] });
|
|
21215
|
+
i++;
|
|
21166
21216
|
continue;
|
|
21167
21217
|
}
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
const indent = Math.floor(listMatch[1].length / 2);
|
|
21171
|
-
const ordered = /\d/.test(listMatch[2]);
|
|
21172
|
-
blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
|
|
21218
|
+
if (/^!\[image\]\([^)]*\)\s*$/.test(line.trim())) {
|
|
21219
|
+
units.push({ kind: "image", raw: line.trim(), lines: [line.trim()] });
|
|
21173
21220
|
i++;
|
|
21174
21221
|
continue;
|
|
21175
21222
|
}
|
|
21176
|
-
|
|
21177
|
-
i
|
|
21223
|
+
const collected = [];
|
|
21224
|
+
while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("|") && !lines[i].trim().startsWith("<table>")) {
|
|
21225
|
+
collected.push(lines[i].trim());
|
|
21226
|
+
i++;
|
|
21227
|
+
}
|
|
21228
|
+
units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
|
|
21178
21229
|
}
|
|
21179
|
-
return
|
|
21230
|
+
return units;
|
|
21180
21231
|
}
|
|
21181
|
-
function
|
|
21182
|
-
|
|
21183
|
-
|
|
21184
|
-
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
|
|
21189
|
-
const idx = match.index;
|
|
21190
|
-
if (idx > lastIdx) {
|
|
21191
|
-
spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
|
|
21232
|
+
function alignUnits(a, b) {
|
|
21233
|
+
const m = a.length, n = b.length;
|
|
21234
|
+
if (m * n > 4e6) {
|
|
21235
|
+
const result2 = [];
|
|
21236
|
+
let pre = 0;
|
|
21237
|
+
while (pre < m && pre < n && a[pre] === b[pre]) {
|
|
21238
|
+
result2.push([pre, pre]);
|
|
21239
|
+
pre++;
|
|
21192
21240
|
}
|
|
21193
|
-
|
|
21194
|
-
|
|
21195
|
-
|
|
21196
|
-
|
|
21197
|
-
|
|
21198
|
-
} else if (raw.startsWith("**") || raw.startsWith("__")) {
|
|
21199
|
-
spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
|
|
21241
|
+
let suf = 0;
|
|
21242
|
+
while (suf < m - pre && suf < n - pre && a[m - 1 - suf] === b[n - 1 - suf]) suf++;
|
|
21243
|
+
const aMid = m - pre - suf, bMid = n - pre - suf;
|
|
21244
|
+
if (aMid === bMid) {
|
|
21245
|
+
for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, pre + i2]);
|
|
21200
21246
|
} else {
|
|
21201
|
-
|
|
21247
|
+
for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, null]);
|
|
21248
|
+
for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
|
|
21202
21249
|
}
|
|
21203
|
-
|
|
21250
|
+
for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
|
|
21251
|
+
return result2;
|
|
21204
21252
|
}
|
|
21205
|
-
|
|
21206
|
-
|
|
21253
|
+
const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
|
|
21254
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
21255
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
21256
|
+
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]);
|
|
21257
|
+
}
|
|
21207
21258
|
}
|
|
21208
|
-
|
|
21209
|
-
|
|
21259
|
+
const matches = [];
|
|
21260
|
+
let i = m, j = n;
|
|
21261
|
+
while (i > 0 && j > 0) {
|
|
21262
|
+
if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
|
|
21263
|
+
matches.push([i - 1, j - 1]);
|
|
21264
|
+
i--;
|
|
21265
|
+
j--;
|
|
21266
|
+
} else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
|
|
21267
|
+
else j--;
|
|
21210
21268
|
}
|
|
21211
|
-
|
|
21212
|
-
|
|
21213
|
-
|
|
21214
|
-
|
|
21215
|
-
|
|
21216
|
-
|
|
21217
|
-
|
|
21218
|
-
|
|
21219
|
-
|
|
21220
|
-
|
|
21221
|
-
|
|
21222
|
-
|
|
21223
|
-
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
21228
|
-
|
|
21229
|
-
|
|
21230
|
-
|
|
21231
|
-
|
|
21232
|
-
|
|
21233
|
-
|
|
21269
|
+
matches.reverse();
|
|
21270
|
+
const result = [];
|
|
21271
|
+
let ai = 0, bi = 0;
|
|
21272
|
+
const flushGap = (aEnd, bEnd) => {
|
|
21273
|
+
if (aEnd - ai === bEnd - bi) {
|
|
21274
|
+
while (ai < aEnd) result.push([ai++, bi++]);
|
|
21275
|
+
return;
|
|
21276
|
+
}
|
|
21277
|
+
while (ai < aEnd && bi < bEnd) {
|
|
21278
|
+
const sim = normalizedSimilarity(a[ai], b[bi]);
|
|
21279
|
+
if (sim >= 0.4) {
|
|
21280
|
+
if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
|
|
21281
|
+
result.push([ai++, null]);
|
|
21282
|
+
} else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
|
|
21283
|
+
result.push([null, bi++]);
|
|
21284
|
+
} else {
|
|
21285
|
+
result.push([ai++, bi++]);
|
|
21286
|
+
}
|
|
21287
|
+
} else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
|
|
21288
|
+
else result.push([null, bi++]);
|
|
21289
|
+
}
|
|
21290
|
+
while (ai < aEnd) result.push([ai++, null]);
|
|
21291
|
+
while (bi < bEnd) result.push([null, bi++]);
|
|
21292
|
+
};
|
|
21293
|
+
for (const [pi, pj] of matches) {
|
|
21294
|
+
flushGap(pi, pj);
|
|
21295
|
+
result.push([ai++, bi++]);
|
|
21234
21296
|
}
|
|
21235
|
-
|
|
21236
|
-
return
|
|
21237
|
-
}
|
|
21238
|
-
function headingParaPrId(level) {
|
|
21239
|
-
if (level === 1) return PARA_H1;
|
|
21240
|
-
if (level === 2) return PARA_H2;
|
|
21241
|
-
if (level === 3) return PARA_H3;
|
|
21242
|
-
return PARA_H4;
|
|
21297
|
+
flushGap(m, n);
|
|
21298
|
+
return result;
|
|
21243
21299
|
}
|
|
21244
|
-
function
|
|
21245
|
-
|
|
21246
|
-
|
|
21247
|
-
|
|
21248
|
-
|
|
21300
|
+
function bestSimInRange(arr, from, to, target) {
|
|
21301
|
+
let best = 0;
|
|
21302
|
+
for (let k = from; k <= to && k < arr.length; k++) {
|
|
21303
|
+
const s = normalizedSimilarity(arr[k], target);
|
|
21304
|
+
if (s > best) best = s;
|
|
21305
|
+
}
|
|
21306
|
+
return best;
|
|
21249
21307
|
}
|
|
21250
|
-
function
|
|
21251
|
-
return
|
|
21252
|
-
<ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
|
|
21253
|
-
<ocf:rootfiles>
|
|
21254
|
-
<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
|
|
21255
|
-
</ocf:rootfiles>
|
|
21256
|
-
</ocf:container>`;
|
|
21308
|
+
function escapeGfm(text) {
|
|
21309
|
+
return text.replace(/~/g, "\\~");
|
|
21257
21310
|
}
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
</opf:package>`;
|
|
21311
|
+
var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
|
|
21312
|
+
function sanitizeText(text) {
|
|
21313
|
+
let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
|
|
21314
|
+
if (result.length <= 30 && result.includes(" ")) {
|
|
21315
|
+
const tokens = result.split(" ");
|
|
21316
|
+
const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-ㄱ-ㆎ]/.test(t)).length;
|
|
21317
|
+
if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
|
|
21318
|
+
result = tokens.join("");
|
|
21319
|
+
}
|
|
21320
|
+
}
|
|
21321
|
+
return result;
|
|
21270
21322
|
}
|
|
21271
|
-
function
|
|
21272
|
-
|
|
21273
|
-
const italicAttr = italic ? ` italic="1"` : "";
|
|
21274
|
-
const effFont = bold ? 2 : fontId;
|
|
21275
|
-
return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
|
|
21276
|
-
<hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
|
|
21277
|
-
<hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
|
|
21278
|
-
<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
21279
|
-
<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
|
|
21280
|
-
<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
|
|
21281
|
-
</hh:charPr>`;
|
|
21323
|
+
function normForMatch(text) {
|
|
21324
|
+
return sanitizeText(text).replace(/\s+/g, " ").trim();
|
|
21282
21325
|
}
|
|
21283
|
-
function
|
|
21284
|
-
|
|
21285
|
-
const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
|
|
21286
|
-
const snapGrid = keepWord ? "0" : "1";
|
|
21287
|
-
return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
|
|
21288
|
-
<hh:align horizontal="${align}" vertical="BASELINE"/>
|
|
21289
|
-
<hh:heading type="NONE" idRef="0" level="0"/>
|
|
21290
|
-
<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
|
|
21291
|
-
<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
|
|
21292
|
-
<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>
|
|
21293
|
-
<hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
|
|
21294
|
-
<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
|
|
21295
|
-
</hh:paraPr>`;
|
|
21326
|
+
function unescapeGfm(text) {
|
|
21327
|
+
return text.replace(/\\~/g, "~");
|
|
21296
21328
|
}
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
var CHAR_VARIANT_BASE = 11;
|
|
21301
|
-
var GONGMUN_BODY_RATIO = 95;
|
|
21302
|
-
function plainRenderText(text) {
|
|
21303
|
-
return parseInlineMarkdown(text).map((s) => s.text).join("");
|
|
21329
|
+
function summarize(text) {
|
|
21330
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
21331
|
+
return t.length > 80 ? t.slice(0, 77) + "..." : t;
|
|
21304
21332
|
}
|
|
21305
|
-
function
|
|
21306
|
-
const
|
|
21307
|
-
if (
|
|
21308
|
-
|
|
21309
|
-
|
|
21310
|
-
const
|
|
21311
|
-
|
|
21312
|
-
|
|
21313
|
-
let
|
|
21314
|
-
|
|
21315
|
-
|
|
21316
|
-
|
|
21317
|
-
|
|
21318
|
-
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
|
|
21323
|
-
|
|
21324
|
-
|
|
21325
|
-
|
|
21326
|
-
|
|
21327
|
-
|
|
21328
|
-
|
|
21333
|
+
function replicateGfmTable(table) {
|
|
21334
|
+
const { cells, rows: numRows, cols: numCols } = table;
|
|
21335
|
+
if (numRows === 0 || numCols === 0) return null;
|
|
21336
|
+
if (numRows === 1 && numCols === 1) return null;
|
|
21337
|
+
if (numCols === 1) return null;
|
|
21338
|
+
const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
|
|
21339
|
+
const skip = /* @__PURE__ */ new Set();
|
|
21340
|
+
for (let r = 0; r < numRows; r++) {
|
|
21341
|
+
for (let c = 0; c < numCols; c++) {
|
|
21342
|
+
if (skip.has(`${r},${c}`)) continue;
|
|
21343
|
+
const cell = cells[r]?.[c];
|
|
21344
|
+
if (!cell) continue;
|
|
21345
|
+
display[r][c] = {
|
|
21346
|
+
text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
|
|
21347
|
+
gridR: r,
|
|
21348
|
+
gridC: c
|
|
21349
|
+
};
|
|
21350
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
21351
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
21352
|
+
if (dr === 0 && dc === 0) continue;
|
|
21353
|
+
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
21354
|
+
}
|
|
21355
|
+
}
|
|
21356
|
+
c += cell.colSpan - 1;
|
|
21357
|
+
}
|
|
21358
|
+
}
|
|
21359
|
+
const uniqueRows = [];
|
|
21360
|
+
let pendingLabelRow = null;
|
|
21361
|
+
for (let r = 0; r < display.length; r++) {
|
|
21362
|
+
const row = display[r];
|
|
21363
|
+
if (row.every((cell) => cell.text === "")) continue;
|
|
21364
|
+
const nonEmptyCols = row.filter((cell) => cell.text !== "");
|
|
21365
|
+
const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
|
|
21366
|
+
if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
|
|
21367
|
+
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
21368
|
+
pendingLabelRow = row;
|
|
21329
21369
|
continue;
|
|
21330
21370
|
}
|
|
21331
|
-
if (
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21371
|
+
if (pendingLabelRow) {
|
|
21372
|
+
if (row[0].text === "") row[0] = pendingLabelRow[0];
|
|
21373
|
+
else uniqueRows.push(pendingLabelRow);
|
|
21374
|
+
pendingLabelRow = null;
|
|
21375
|
+
}
|
|
21376
|
+
uniqueRows.push(row);
|
|
21336
21377
|
}
|
|
21337
|
-
|
|
21378
|
+
if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
|
|
21379
|
+
return uniqueRows.length > 0 ? uniqueRows : null;
|
|
21338
21380
|
}
|
|
21339
|
-
function
|
|
21340
|
-
const
|
|
21341
|
-
|
|
21342
|
-
|
|
21343
|
-
|
|
21381
|
+
function parseGfmTable(lines) {
|
|
21382
|
+
const rows = [];
|
|
21383
|
+
for (const line of lines) {
|
|
21384
|
+
const trimmed = line.trim();
|
|
21385
|
+
if (!trimmed.startsWith("|")) continue;
|
|
21386
|
+
const cells = trimmed.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim());
|
|
21387
|
+
if (cells.length === 0) continue;
|
|
21388
|
+
if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
|
|
21389
|
+
rows.push(cells);
|
|
21390
|
+
}
|
|
21391
|
+
return rows;
|
|
21344
21392
|
}
|
|
21345
|
-
function
|
|
21346
|
-
|
|
21347
|
-
|
|
21348
|
-
|
|
21349
|
-
|
|
21350
|
-
|
|
21351
|
-
|
|
21352
|
-
|
|
21353
|
-
|
|
21393
|
+
function unescapeGfmCell(text) {
|
|
21394
|
+
return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\~/g, "~");
|
|
21395
|
+
}
|
|
21396
|
+
function replicateCellInnerHtml(cell) {
|
|
21397
|
+
if (cell.blocks?.length) {
|
|
21398
|
+
return cell.blocks.map((b) => {
|
|
21399
|
+
if (b.type === "table" && b.table) {
|
|
21400
|
+
const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
|
|
21401
|
+
return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
|
|
21402
|
+
}
|
|
21403
|
+
if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
|
|
21404
|
+
const t = sanitizeText(b.text ?? "");
|
|
21405
|
+
return t ? t.replace(/\n/g, "<br>") : "";
|
|
21406
|
+
}).filter(Boolean).join("<br>");
|
|
21354
21407
|
}
|
|
21355
|
-
|
|
21356
|
-
|
|
21357
|
-
|
|
21358
|
-
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
|
|
21375
|
-
|
|
21408
|
+
return sanitizeText(cell.text).replace(/\n/g, "<br>");
|
|
21409
|
+
}
|
|
21410
|
+
function replicateTableToHtml(table) {
|
|
21411
|
+
const rows = replicateHtmlTable(table);
|
|
21412
|
+
const lines = ["<table>"];
|
|
21413
|
+
for (let r = 0; r < rows.length; r++) {
|
|
21414
|
+
const tag = rows[r].tag;
|
|
21415
|
+
const rowHtml = rows[r].cells.map((cell) => {
|
|
21416
|
+
const attrs = [];
|
|
21417
|
+
if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
|
|
21418
|
+
if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
|
|
21419
|
+
const attrStr = attrs.length ? " " + attrs.join(" ") : "";
|
|
21420
|
+
return `<${tag}${attrStr}>${cell.inner}</${tag}>`;
|
|
21421
|
+
});
|
|
21422
|
+
if (rowHtml.length) lines.push(`<tr>${rowHtml.join("")}</tr>`);
|
|
21423
|
+
}
|
|
21424
|
+
lines.push("</table>");
|
|
21425
|
+
return lines.join("\n");
|
|
21426
|
+
}
|
|
21427
|
+
function replicateHtmlTable(table) {
|
|
21428
|
+
const { cells, rows: numRows, cols: numCols } = table;
|
|
21429
|
+
const skip = /* @__PURE__ */ new Set();
|
|
21430
|
+
const result = [];
|
|
21431
|
+
for (let r = 0; r < numRows; r++) {
|
|
21432
|
+
const tag = r === 0 ? "th" : "td";
|
|
21433
|
+
const rowCells = [];
|
|
21434
|
+
for (let c = 0; c < numCols; c++) {
|
|
21435
|
+
if (skip.has(`${r},${c}`)) continue;
|
|
21436
|
+
const cell = cells[r]?.[c];
|
|
21437
|
+
if (!cell) continue;
|
|
21438
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
21439
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
21440
|
+
if (dr === 0 && dc === 0) continue;
|
|
21441
|
+
if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
|
|
21442
|
+
}
|
|
21443
|
+
}
|
|
21444
|
+
rowCells.push({
|
|
21445
|
+
inner: replicateCellInnerHtml(cell),
|
|
21446
|
+
colSpan: cell.colSpan,
|
|
21447
|
+
rowSpan: cell.rowSpan,
|
|
21448
|
+
gridR: r,
|
|
21449
|
+
gridC: c
|
|
21450
|
+
});
|
|
21451
|
+
}
|
|
21452
|
+
if (rowCells.length) result.push({ tag, cells: rowCells });
|
|
21453
|
+
}
|
|
21454
|
+
return result;
|
|
21455
|
+
}
|
|
21456
|
+
function parseHtmlTable(raw) {
|
|
21457
|
+
const re = /<(\/?)(table|tr|td|th)((?:"[^"]*"|'[^']*'|[^>"'])*?)>/gi;
|
|
21458
|
+
let depth = 0;
|
|
21459
|
+
let currentRow = null;
|
|
21460
|
+
let cellStart = -1;
|
|
21461
|
+
let cellInfo = null;
|
|
21462
|
+
const rows = [];
|
|
21463
|
+
let m;
|
|
21464
|
+
while ((m = re.exec(raw)) !== null) {
|
|
21465
|
+
const isClose = m[1] === "/";
|
|
21466
|
+
const tag = m[2].toLowerCase();
|
|
21467
|
+
const attrs = m[3] || "";
|
|
21468
|
+
if (tag === "table") {
|
|
21469
|
+
depth += isClose ? -1 : 1;
|
|
21470
|
+
if (depth < 0) return null;
|
|
21471
|
+
continue;
|
|
21472
|
+
}
|
|
21473
|
+
if (depth !== 1) continue;
|
|
21474
|
+
if (tag === "tr") {
|
|
21475
|
+
if (!isClose) currentRow = [];
|
|
21476
|
+
else if (currentRow) {
|
|
21477
|
+
rows.push({ tag: rows.length === 0 ? "th" : "td", cells: currentRow });
|
|
21478
|
+
currentRow = null;
|
|
21479
|
+
}
|
|
21480
|
+
} else {
|
|
21481
|
+
if (!isClose) {
|
|
21482
|
+
const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
21483
|
+
const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
|
|
21484
|
+
cellStart = m.index + m[0].length;
|
|
21485
|
+
cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
|
|
21486
|
+
} else if (cellStart >= 0 && cellInfo && currentRow) {
|
|
21487
|
+
currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
|
|
21488
|
+
cellStart = -1;
|
|
21489
|
+
cellInfo = null;
|
|
21490
|
+
}
|
|
21491
|
+
}
|
|
21376
21492
|
}
|
|
21377
|
-
return
|
|
21378
|
-
|
|
21379
|
-
</hh:charProperties>`;
|
|
21493
|
+
if (depth !== 0) return null;
|
|
21494
|
+
return rows;
|
|
21380
21495
|
}
|
|
21381
|
-
|
|
21382
|
-
|
|
21383
|
-
|
|
21384
|
-
|
|
21385
|
-
|
|
21386
|
-
|
|
21387
|
-
|
|
21388
|
-
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
|
|
21389
|
-
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
|
|
21390
|
-
paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
|
|
21391
|
-
paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
|
|
21392
|
-
];
|
|
21393
|
-
return `<hh:paraProperties itemCnt="${base2.length}">
|
|
21394
|
-
${base2.join("\n")}
|
|
21395
|
-
</hh:paraProperties>`;
|
|
21496
|
+
var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-9a-zA-Z가-힣]{1,6}[)\]][.:]?|[ⅰ-ⅹⅠ-Ⅹ①-⑮][.)\]:]?)$/u;
|
|
21497
|
+
function htmlCellInnerToLines(inner) {
|
|
21498
|
+
let hadNonText = false;
|
|
21499
|
+
let work = inner;
|
|
21500
|
+
if (/<table[\s>]/i.test(work)) {
|
|
21501
|
+
hadNonText = true;
|
|
21502
|
+
work = removeNestedTables(work);
|
|
21396
21503
|
}
|
|
21397
|
-
|
|
21398
|
-
|
|
21399
|
-
|
|
21400
|
-
paraPr(0, { lineSpacing: ls, keepWord: true }),
|
|
21401
|
-
paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
|
|
21402
|
-
paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
|
|
21403
|
-
paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
21404
|
-
paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
|
|
21405
|
-
paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
|
|
21406
|
-
paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
|
|
21407
|
-
paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
|
|
21408
|
-
];
|
|
21409
|
-
for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
|
|
21410
|
-
const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
|
|
21411
|
-
const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
|
|
21412
|
-
base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
|
|
21504
|
+
if (/<img\s/i.test(work)) {
|
|
21505
|
+
hadNonText = true;
|
|
21506
|
+
work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
|
|
21413
21507
|
}
|
|
21414
|
-
|
|
21415
|
-
return
|
|
21416
|
-
${base.join("\n")}
|
|
21417
|
-
</hh:paraProperties>`;
|
|
21508
|
+
const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
21509
|
+
return { lines, hadNonText };
|
|
21418
21510
|
}
|
|
21419
|
-
function
|
|
21420
|
-
const
|
|
21421
|
-
|
|
21422
|
-
|
|
21423
|
-
|
|
21424
|
-
|
|
21425
|
-
|
|
21426
|
-
|
|
21427
|
-
|
|
21428
|
-
|
|
21429
|
-
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
|
|
21436
|
-
|
|
21437
|
-
|
|
21438
|
-
|
|
21439
|
-
<hh:fontface lang="LATIN" fontCnt="3">
|
|
21440
|
-
<hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
|
|
21441
|
-
<hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
|
|
21442
|
-
</hh:font>
|
|
21443
|
-
<hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
|
|
21444
|
-
<hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
21445
|
-
</hh:font>
|
|
21446
|
-
<hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
|
|
21447
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
|
|
21448
|
-
</hh:font>
|
|
21449
|
-
</hh:fontface>
|
|
21450
|
-
<hh:fontface lang="HANJA" fontCnt="1">
|
|
21451
|
-
<hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
|
|
21452
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21453
|
-
</hh:font>
|
|
21454
|
-
</hh:fontface>
|
|
21455
|
-
<hh:fontface lang="JAPANESE" fontCnt="1">
|
|
21456
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21457
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21458
|
-
</hh:font>
|
|
21459
|
-
</hh:fontface>
|
|
21460
|
-
<hh:fontface lang="OTHER" fontCnt="1">
|
|
21461
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21462
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21463
|
-
</hh:font>
|
|
21464
|
-
</hh:fontface>
|
|
21465
|
-
<hh:fontface lang="SYMBOL" fontCnt="1">
|
|
21466
|
-
<hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
|
|
21467
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21468
|
-
</hh:font>
|
|
21469
|
-
</hh:fontface>
|
|
21470
|
-
<hh:fontface lang="USER" fontCnt="1">
|
|
21471
|
-
<hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
|
|
21472
|
-
<hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
|
|
21473
|
-
</hh:font>
|
|
21474
|
-
</hh:fontface>
|
|
21475
|
-
</hh:fontfaces>
|
|
21476
|
-
<hh:borderFills itemCnt="2">
|
|
21477
|
-
<hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21478
|
-
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21479
|
-
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21480
|
-
<hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21481
|
-
<hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21482
|
-
<hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21483
|
-
<hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
|
|
21484
|
-
</hh:borderFill>
|
|
21485
|
-
<hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
|
|
21486
|
-
<hh:slash type="NONE" Crooked="0" isCounter="0"/>
|
|
21487
|
-
<hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
|
|
21488
|
-
<hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21489
|
-
<hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21490
|
-
<hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21491
|
-
<hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
|
|
21492
|
-
</hh:borderFill>
|
|
21493
|
-
</hh:borderFills>
|
|
21494
|
-
${charPropsXml}
|
|
21495
|
-
<hh:tabProperties itemCnt="0"/>
|
|
21496
|
-
<hh:numberings itemCnt="0"/>
|
|
21497
|
-
<hh:bullets itemCnt="0"/>
|
|
21498
|
-
${paraPropsXml}
|
|
21499
|
-
<hh:styles itemCnt="1">
|
|
21500
|
-
<hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
|
|
21501
|
-
</hh:styles>
|
|
21502
|
-
</hh:refList>
|
|
21503
|
-
<hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
|
|
21504
|
-
</hh:head>`;
|
|
21511
|
+
function extractTopLevelTables(html) {
|
|
21512
|
+
const result = [];
|
|
21513
|
+
let depth = 0;
|
|
21514
|
+
let start = -1;
|
|
21515
|
+
const re = /<(\/?)table(?:[\s>]|>)/gi;
|
|
21516
|
+
let m;
|
|
21517
|
+
while ((m = re.exec(html)) !== null) {
|
|
21518
|
+
if (m[1] !== "/") {
|
|
21519
|
+
if (depth === 0) start = m.index;
|
|
21520
|
+
depth++;
|
|
21521
|
+
} else {
|
|
21522
|
+
depth--;
|
|
21523
|
+
if (depth === 0 && start >= 0) {
|
|
21524
|
+
result.push(html.slice(start, m.index + m[0].length));
|
|
21525
|
+
start = -1;
|
|
21526
|
+
}
|
|
21527
|
+
if (depth < 0) depth = 0;
|
|
21528
|
+
}
|
|
21529
|
+
}
|
|
21530
|
+
return result;
|
|
21505
21531
|
}
|
|
21506
|
-
function
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21532
|
+
function removeNestedTables(html) {
|
|
21533
|
+
let result = "";
|
|
21534
|
+
let depth = 0;
|
|
21535
|
+
const re = /<(\/?)table(?:[\s>]|>)/gi;
|
|
21536
|
+
let last = 0;
|
|
21537
|
+
let m;
|
|
21538
|
+
while ((m = re.exec(html)) !== null) {
|
|
21539
|
+
if (m[1] !== "/") {
|
|
21540
|
+
if (depth === 0) result += html.slice(last, m.index);
|
|
21541
|
+
depth++;
|
|
21542
|
+
} else {
|
|
21543
|
+
depth--;
|
|
21544
|
+
if (depth === 0) last = m.index + m[0].length;
|
|
21545
|
+
if (depth < 0) depth = 0;
|
|
21546
|
+
}
|
|
21547
|
+
}
|
|
21548
|
+
if (depth === 0) result += html.slice(last);
|
|
21549
|
+
return result;
|
|
21516
21550
|
}
|
|
21551
|
+
|
|
21552
|
+
// src/hwpx/gen-table.ts
|
|
21517
21553
|
var TABLE_ID_BASE = 1e3;
|
|
21518
21554
|
var tableIdCounter = TABLE_ID_BASE;
|
|
21519
21555
|
function nextTableId() {
|
|
@@ -21603,41 +21639,18 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3) {
|
|
|
21603
21639
|
}
|
|
21604
21640
|
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>`;
|
|
21605
21641
|
}
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
21611
|
-
|
|
21612
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
run.push(i);
|
|
21619
|
-
i++;
|
|
21620
|
-
continue;
|
|
21621
|
-
}
|
|
21622
|
-
if (t === "table" || t === "html_table") {
|
|
21623
|
-
let j = i + 1;
|
|
21624
|
-
while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
|
|
21625
|
-
if (j < blocks.length && blocks[j].type === "list_item") {
|
|
21626
|
-
i = j;
|
|
21627
|
-
continue;
|
|
21628
|
-
}
|
|
21629
|
-
}
|
|
21630
|
-
break;
|
|
21631
|
-
}
|
|
21632
|
-
const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
|
|
21633
|
-
const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
|
|
21634
|
-
const numberer = new GongmunNumberer(gongmun.numbering);
|
|
21635
|
-
run.forEach((bi, k) => {
|
|
21636
|
-
const marker = numberer.next(depths[k], suppress[k]);
|
|
21637
|
-
result.set(bi, { marker, depth: depths[k] });
|
|
21638
|
-
});
|
|
21639
|
-
}
|
|
21640
|
-
return result;
|
|
21642
|
+
|
|
21643
|
+
// src/hwpx/gen-section.ts
|
|
21644
|
+
function generateSecPr(gongmun) {
|
|
21645
|
+
const m = gongmun ? {
|
|
21646
|
+
top: mmToHwpunit(gongmun.margins.top),
|
|
21647
|
+
bottom: mmToHwpunit(gongmun.margins.bottom),
|
|
21648
|
+
left: mmToHwpunit(gongmun.margins.left),
|
|
21649
|
+
right: mmToHwpunit(gongmun.margins.right),
|
|
21650
|
+
header: 0,
|
|
21651
|
+
footer: 0
|
|
21652
|
+
} : { top: 8504, bottom: 4252, left: 5670, right: 4252, header: 2835, footer: 2835 };
|
|
21653
|
+
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>`;
|
|
21641
21654
|
}
|
|
21642
21655
|
function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null) {
|
|
21643
21656
|
const paraXmls = [];
|
|
@@ -21760,6 +21773,24 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
|
|
|
21760
21773
|
</hs:sec>`;
|
|
21761
21774
|
}
|
|
21762
21775
|
|
|
21776
|
+
// src/hwpx/generator.ts
|
|
21777
|
+
async function markdownToHwpx(markdown, options) {
|
|
21778
|
+
const theme = resolveTheme(options?.theme);
|
|
21779
|
+
const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
|
|
21780
|
+
const blocks = parseMarkdownToBlocks(markdown);
|
|
21781
|
+
const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
|
|
21782
|
+
const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
|
|
21783
|
+
const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
|
|
21784
|
+
const zip = new JSZip7();
|
|
21785
|
+
zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
|
|
21786
|
+
zip.file("META-INF/container.xml", generateContainerXml());
|
|
21787
|
+
zip.file("Contents/content.hpf", generateManifest());
|
|
21788
|
+
zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
|
|
21789
|
+
zip.file("Contents/section0.xml", sectionXml);
|
|
21790
|
+
zip.file("Preview/PrvText.txt", buildPrvText(blocks));
|
|
21791
|
+
return await zip.generateAsync({ type: "arraybuffer" });
|
|
21792
|
+
}
|
|
21793
|
+
|
|
21763
21794
|
// src/diff/compare.ts
|
|
21764
21795
|
var SIMILARITY_THRESHOLD = 0.4;
|
|
21765
21796
|
async function compare(bufferA, bufferB, options) {
|
|
@@ -21895,7 +21926,7 @@ function diffTableCells(a, b) {
|
|
|
21895
21926
|
}
|
|
21896
21927
|
|
|
21897
21928
|
// src/roundtrip/patcher.ts
|
|
21898
|
-
import
|
|
21929
|
+
import JSZip8 from "jszip";
|
|
21899
21930
|
|
|
21900
21931
|
// src/roundtrip/table-rows.ts
|
|
21901
21932
|
var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
|
|
@@ -22533,7 +22564,7 @@ async function patchHwpx(original, editedMarkdown, options) {
|
|
|
22533
22564
|
}
|
|
22534
22565
|
let zip;
|
|
22535
22566
|
try {
|
|
22536
|
-
zip = await
|
|
22567
|
+
zip = await JSZip8.loadAsync(original);
|
|
22537
22568
|
} catch {
|
|
22538
22569
|
return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
|
|
22539
22570
|
}
|
|
@@ -23900,10 +23931,10 @@ function stageParaPatch(scan, para, newPlain, skip) {
|
|
|
23900
23931
|
}
|
|
23901
23932
|
|
|
23902
23933
|
// src/roundtrip/session.ts
|
|
23903
|
-
import
|
|
23934
|
+
import JSZip9 from "jszip";
|
|
23904
23935
|
async function buildState(bytes) {
|
|
23905
23936
|
const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
|
|
23906
|
-
const zip = await
|
|
23937
|
+
const zip = await JSZip9.loadAsync(bytes);
|
|
23907
23938
|
const sectionPaths = await resolveSectionEntryNames(zip);
|
|
23908
23939
|
if (sectionPaths.length === 0) {
|
|
23909
23940
|
throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
@@ -24484,7 +24515,7 @@ async function parseHwp(buffer, options) {
|
|
|
24484
24515
|
async function parsePdf(buffer, options) {
|
|
24485
24516
|
let parsePdfDocument;
|
|
24486
24517
|
try {
|
|
24487
|
-
const mod = await import("./parser-
|
|
24518
|
+
const mod = await import("./parser-NR2TYGO3.js");
|
|
24488
24519
|
parsePdfDocument = mod.parsePdfDocument;
|
|
24489
24520
|
} catch {
|
|
24490
24521
|
return {
|