kordoc 3.8.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +6 -1
  2. package/dist/{-ATVQYFSW.js → -LD4BZDDJ.js} +3 -3
  3. package/dist/{chunk-3R3YK7EM.js → chunk-IFYJFWD2.js} +2 -2
  4. package/dist/{chunk-SLKF72QF.js → chunk-KT2BCHXI.js} +690 -671
  5. package/dist/chunk-KT2BCHXI.js.map +1 -0
  6. package/dist/{chunk-ITJIALN5.cjs → chunk-LFCS3UVG.cjs} +2 -2
  7. package/dist/{chunk-ITJIALN5.cjs.map → chunk-LFCS3UVG.cjs.map} +1 -1
  8. package/dist/{chunk-QV25HMU7.js → chunk-PELBIL4K.js} +2 -2
  9. package/dist/cli.js +4 -4
  10. package/dist/index.cjs +1052 -1033
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +689 -670
  13. package/dist/index.js.map +1 -1
  14. package/dist/mcp.js +3 -3
  15. package/dist/{parser-7G5F7PT2.js → parser-FFEBMLSH.js} +1830 -1803
  16. package/dist/parser-FFEBMLSH.js.map +1 -0
  17. package/dist/{parser-GUSJH44K.cjs → parser-IXK5V7YG.cjs} +1830 -1803
  18. package/dist/parser-IXK5V7YG.cjs.map +1 -0
  19. package/dist/{parser-DCK42RMA.js → parser-XEDROIM7.js} +1830 -1803
  20. package/dist/parser-XEDROIM7.js.map +1 -0
  21. package/dist/{watch-GVZESOCE.js → watch-MAWCDNFI.js} +3 -3
  22. package/package.json +2 -2
  23. package/dist/chunk-SLKF72QF.js.map +0 -1
  24. package/dist/parser-7G5F7PT2.js.map +0 -1
  25. package/dist/parser-DCK42RMA.js.map +0 -1
  26. package/dist/parser-GUSJH44K.cjs.map +0 -1
  27. /package/dist/{-ATVQYFSW.js.map → -LD4BZDDJ.js.map} +0 -0
  28. /package/dist/{chunk-3R3YK7EM.js.map → chunk-IFYJFWD2.js.map} +0 -0
  29. /package/dist/{chunk-QV25HMU7.js.map → chunk-PELBIL4K.js.map} +0 -0
  30. /package/dist/{watch-GVZESOCE.js.map → watch-MAWCDNFI.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-QV25HMU7.js";
22
+ } from "./chunk-PELBIL4K.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 JSZip2 from "jszip";
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,629 +1066,101 @@ function hmlToLatex(hmlEqStr) {
829
1066
  return out;
830
1067
  }
831
1068
 
832
- // src/hwpx/parser.ts
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
- }
999
- var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
1000
- var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
1001
- function toRoman(n) {
1002
- if (n <= 0 || n >= 4e3) return String(n);
1003
- const table = [
1004
- [1e3, "M"],
1005
- [900, "CM"],
1006
- [500, "D"],
1007
- [400, "CD"],
1008
- [100, "C"],
1009
- [90, "XC"],
1010
- [50, "L"],
1011
- [40, "XL"],
1012
- [10, "X"],
1013
- [9, "IX"],
1014
- [5, "V"],
1015
- [4, "IV"],
1016
- [1, "I"]
1017
- ];
1018
- let out = "";
1019
- for (const [v, s] of table) {
1020
- while (n >= v) {
1021
- out += s;
1022
- n -= v;
1023
- }
1024
- }
1025
- return out;
1026
- }
1027
- function formatHeadNumber(n, numFormat) {
1028
- if (n <= 0) n = 1;
1029
- switch (numFormat) {
1030
- case "DIGIT":
1031
- return String(n);
1032
- case "CIRCLED_DIGIT":
1033
- return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
1034
- case "HANGUL_SYLLABLE":
1035
- return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
1036
- case "CIRCLED_HANGUL_SYLLABLE":
1037
- return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
1038
- case "HANGUL_JAMO":
1039
- return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
1040
- case "CIRCLED_HANGUL_JAMO":
1041
- return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
1042
- case "LATIN_CAPITAL":
1043
- return String.fromCharCode(65 + (n - 1) % 26);
1044
- case "LATIN_SMALL":
1045
- return String.fromCharCode(97 + (n - 1) % 26);
1046
- case "CIRCLED_LATIN_CAPITAL":
1047
- return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
1048
- case "CIRCLED_LATIN_SMALL":
1049
- return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
1050
- case "ROMAN_CAPITAL":
1051
- return toRoman(n);
1052
- case "ROMAN_SMALL":
1053
- return toRoman(n).toLowerCase();
1054
- default:
1055
- return String(n);
1056
- }
1057
- }
1058
- function resolveParaHeading(paraEl, ctx) {
1059
- const sm = ctx.styleMap;
1060
- if (!sm) return null;
1061
- const prId = paraEl.getAttribute("paraPrIDRef");
1062
- if (!prId) return null;
1063
- const ref = sm.paraHeadings.get(prId);
1064
- if (!ref) return null;
1065
- if (ref.type === "BULLET") {
1066
- const char = sm.bullets.get(ref.idRef);
1067
- return char ? { prefix: char } : null;
1068
- }
1069
- const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
1070
- const level = Math.min(ref.level + 1, 10);
1071
- const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
1072
- const numDef = sm.numberings.get(numId);
1073
- if (!numDef) return headingLevel ? { headingLevel } : null;
1074
- let counters = ctx.shared.numState.get(numId);
1075
- if (!counters) {
1076
- counters = new Array(11).fill(0);
1077
- ctx.shared.numState.set(numId, counters);
1078
- }
1079
- const head = numDef.heads.get(level);
1080
- counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
1081
- for (let l = level + 1; l <= 10; l++) counters[l] = 0;
1082
- const fmtText = head?.text?.trim() || `^${level}.`;
1083
- const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1084
- const lv = parseInt(d, 10);
1085
- const refHead = numDef.heads.get(lv);
1086
- const n = counters[lv] || refHead?.start || 1;
1087
- return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1088
- });
1089
- return { prefix, headingLevel };
1090
- }
1091
- async function parseHwpxDocument(buffer, options) {
1092
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
1093
- let zip;
1094
- try {
1095
- zip = await JSZip2.loadAsync(buffer);
1096
- } catch {
1097
- return extractFromBrokenZip(buffer);
1098
- }
1099
- const actualEntryCount = Object.keys(zip.files).length;
1100
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
1101
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1102
- }
1103
- const manifestFile = zip.file("META-INF/manifest.xml");
1104
- if (manifestFile) {
1105
- const manifestXml = await manifestFile.async("text");
1106
- if (isEncryptedHwpx(manifestXml)) {
1107
- if (isComFallbackAvailable() && options?.filePath) {
1108
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
1109
- if (pages.some((p) => p && p.trim().length > 0)) {
1110
- return comResultToParseResult(pages, pageCount, warnings2);
1111
- }
1112
- }
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
- }
1115
- }
1116
- const decompressed = { total: 0 };
1117
- const metadata = {};
1118
- await extractHwpxMetadata(zip, metadata, decompressed);
1119
- const styleMap = await extractHwpxStyles(zip, decompressed);
1120
- const warnings = [];
1121
- const sectionPaths = await resolveSectionPaths(zip);
1122
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1123
- metadata.pageCount = sectionPaths.length;
1124
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
1125
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
1126
- const blocks = [];
1127
- const shared = createSectionShared();
1128
- let parsedSections = 0;
1129
- for (let si = 0; si < sectionPaths.length; si++) {
1130
- if (pageFilter && !pageFilter.has(si + 1)) continue;
1131
- const file = zip.file(sectionPaths[si]);
1132
- if (!file) continue;
1133
- try {
1134
- const xml = await file.async("text");
1135
- decompressed.total += xml.length * 2;
1136
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1137
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
1138
- parsedSections++;
1139
- options?.onProgress?.(parsedSections, totalTarget);
1140
- } catch (secErr) {
1141
- if (secErr instanceof KordocError) throw secErr;
1142
- 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" });
1143
- }
1144
- }
1145
- applyPageText(blocks, shared);
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 };
1151
- }
1152
- function applyPageText(blocks, shared) {
1153
- const { headers, footers } = shared.pageText;
1154
- if (headers.length > 0) {
1155
- blocks.unshift(...headers.map((t) => ({ type: "paragraph", text: t, pageNumber: 1 })));
1156
- }
1157
- if (footers.length > 0) {
1158
- blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
1159
- }
1160
- }
1161
- function imageExtToMime(ext) {
1162
- switch (ext.toLowerCase()) {
1163
- case "jpg":
1164
- case "jpeg":
1165
- return "image/jpeg";
1166
- case "png":
1167
- return "image/png";
1168
- case "gif":
1169
- return "image/gif";
1170
- case "bmp":
1171
- return "image/bmp";
1172
- case "tif":
1173
- case "tiff":
1174
- return "image/tiff";
1175
- case "wmf":
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";
1183
- }
1184
- }
1185
- function mimeToExt(mime) {
1186
- if (mime.includes("jpeg")) return "jpg";
1187
- if (mime.includes("png")) return "png";
1188
- if (mime.includes("gif")) return "gif";
1189
- if (mime.includes("bmp")) return "bmp";
1190
- if (mime.includes("tiff")) return "tif";
1191
- if (mime.includes("wmf")) return "wmf";
1192
- if (mime.includes("emf")) return "emf";
1193
- if (mime.includes("svg")) return "svg";
1194
- return "bin";
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
- }
1207
- }
1208
- }
1209
- }
1210
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
1211
- const images = [];
1212
- let imageIndex = 0;
1213
- const imageBlocks = [];
1214
- collectImageBlocks(blocks, imageBlocks);
1215
- const resolved = /* @__PURE__ */ new Map();
1216
- for (const { block, ownerCell } of imageBlocks) {
1217
- if (block.type !== "image" || !block.text) continue;
1218
- const ref = block.text;
1219
- let img = resolved.get(ref);
1220
- if (img === void 0) {
1221
- img = null;
1222
- const candidates = [
1223
- `BinData/${ref}`,
1224
- `Contents/BinData/${ref}`,
1225
- ref
1226
- // 절대 경로일 수도 있음
1227
- ];
1228
- let resolvedPath = null;
1229
- if (!ref.includes(".")) {
1230
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
1231
- for (const prefix of prefixes) {
1232
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
1233
- if (match.length > 0) {
1234
- resolvedPath = match[0].name;
1235
- break;
1236
- }
1237
- }
1238
- }
1239
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
1240
- for (const path of allCandidates) {
1241
- if (isPathTraversal(path)) continue;
1242
- const file = zip.file(path);
1243
- if (!file) continue;
1244
- try {
1245
- const data = await file.async("uint8array");
1246
- decompressed.total += data.length;
1247
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1248
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
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;
1257
- }
1258
- }
1259
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
1260
- resolved.set(ref, img);
1261
- }
1262
- if (!img) {
1263
- block.type = "paragraph";
1264
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
1265
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\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(`![image](${ref})`, `![image](${img.filename})`);
1271
- }
1272
- return images;
1273
- }
1274
- async function extractHwpxMetadata(zip, metadata, decompressed) {
1275
- try {
1276
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
1277
- for (const mp of metaPaths) {
1278
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
1279
- if (!file) continue;
1280
- const xml = await file.async("text");
1281
- if (decompressed) {
1282
- decompressed.total += xml.length * 2;
1283
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1284
- }
1285
- parseDublinCoreMetadata(xml, metadata);
1286
- if (metadata.title || metadata.author) return;
1287
- }
1288
- } catch {
1289
- }
1290
- }
1291
- function parseDublinCoreMetadata(xml, metadata) {
1292
- const parser = createXmlParser();
1293
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1294
- if (!doc.documentElement) return;
1295
- const getText = (tagNames) => {
1296
- for (const tag of tagNames) {
1297
- const els = doc.getElementsByTagName(tag);
1298
- if (els.length > 0) {
1299
- const text = els[0].textContent?.trim();
1300
- if (text) return text;
1301
- }
1302
- }
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
- }
1314
- }
1315
- function extractFromBrokenZip(buffer) {
1316
- const data = new Uint8Array(buffer);
1317
- const view = new DataView(buffer);
1318
- let pos = 0;
1319
- const blocks = [];
1320
- const warnings = [
1321
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
1322
- ];
1323
- let totalDecompressed = 0;
1324
- let entryCount = 0;
1325
- let sectionNum = 0;
1326
- const shared = createSectionShared();
1327
- while (pos < data.length - 30) {
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;
1355
- continue;
1356
- }
1357
- const fileData = data.slice(fileStart, fileStart + compSize);
1358
- pos = fileStart + compSize;
1359
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
1360
- try {
1361
- let content;
1362
- if (method === 0) {
1363
- content = new TextDecoder().decode(fileData);
1364
- } else if (method === 8) {
1365
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
1366
- content = new TextDecoder().decode(decompressed);
1367
- } else {
1368
- continue;
1369
- }
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
- }
1377
- }
1378
- 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");
1379
- applyPageText(blocks, shared);
1380
- const markdown = blocksToMarkdown(blocks);
1381
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1382
- }
1383
- async function resolveSectionPaths(zip) {
1384
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1385
- for (const mp of manifestPaths) {
1386
- const mpLower = mp.toLowerCase();
1387
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
1388
- if (!file) continue;
1389
- const xml = await file.async("text");
1390
- const paths = parseSectionPathsFromManifest(xml);
1391
- if (paths.length > 0) return paths;
1392
- }
1393
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
1394
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
1395
- }
1396
- function parseSectionPathsFromManifest(xml) {
1397
- const parser = createXmlParser();
1398
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1399
- const items = doc.getElementsByTagName("opf:item");
1400
- const spine = doc.getElementsByTagName("opf:itemref");
1401
- const idToHref = /* @__PURE__ */ new Map();
1402
- for (let i = 0; i < items.length; i++) {
1403
- const item = items[i];
1404
- const id = item.getAttribute("id") || "";
1405
- const href = normalizeSectionHref(item.getAttribute("href") || "");
1406
- if (id && href) idToHref.set(id, href);
1407
- }
1408
- if (spine.length > 0) {
1409
- const ordered = [];
1410
- for (let i = 0; i < spine.length; i++) {
1411
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
1412
- if (href) ordered.push(href);
1069
+ // src/hwpx/para-heading.ts
1070
+ var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
1071
+ var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
1072
+ function toRoman(n) {
1073
+ if (n <= 0 || n >= 4e3) return String(n);
1074
+ const table = [
1075
+ [1e3, "M"],
1076
+ [900, "CM"],
1077
+ [500, "D"],
1078
+ [400, "CD"],
1079
+ [100, "C"],
1080
+ [90, "XC"],
1081
+ [50, "L"],
1082
+ [40, "XL"],
1083
+ [10, "X"],
1084
+ [9, "IX"],
1085
+ [5, "V"],
1086
+ [4, "IV"],
1087
+ [1, "I"]
1088
+ ];
1089
+ let out = "";
1090
+ for (const [v, s] of table) {
1091
+ while (n >= v) {
1092
+ out += s;
1093
+ n -= v;
1413
1094
  }
1414
- if (ordered.length > 0) return ordered;
1415
1095
  }
1416
- return Array.from(idToHref.values()).sort(compareSectionPaths);
1096
+ return out;
1417
1097
  }
1418
- function detectHwpxHeadings(blocks, styleMap) {
1419
- if (blocks.some((b) => b.type === "heading")) return;
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
- }
1098
+ function formatHeadNumber(n, numFormat) {
1099
+ if (n <= 0) n = 1;
1100
+ switch (numFormat) {
1101
+ case "DIGIT":
1102
+ return String(n);
1103
+ case "CIRCLED_DIGIT":
1104
+ return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
1105
+ case "HANGUL_SYLLABLE":
1106
+ return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
1107
+ case "CIRCLED_HANGUL_SYLLABLE":
1108
+ return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
1109
+ case "HANGUL_JAMO":
1110
+ return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
1111
+ case "CIRCLED_HANGUL_JAMO":
1112
+ return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
1113
+ case "LATIN_CAPITAL":
1114
+ return String.fromCharCode(65 + (n - 1) % 26);
1115
+ case "LATIN_SMALL":
1116
+ return String.fromCharCode(97 + (n - 1) % 26);
1117
+ case "CIRCLED_LATIN_CAPITAL":
1118
+ return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
1119
+ case "CIRCLED_LATIN_SMALL":
1120
+ return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
1121
+ case "ROMAN_CAPITAL":
1122
+ return toRoman(n);
1123
+ case "ROMAN_SMALL":
1124
+ return toRoman(n).toLowerCase();
1125
+ default:
1126
+ return String(n);
1426
1127
  }
1427
- let maxCount = 0;
1428
- for (const [size, count] of sizeFreq) {
1429
- if (count > maxCount) {
1430
- maxCount = count;
1431
- baseFontSize = size;
1432
- }
1128
+ }
1129
+ function resolveParaHeading(paraEl, ctx) {
1130
+ const sm = ctx.styleMap;
1131
+ if (!sm) return null;
1132
+ const prId = paraEl.getAttribute("paraPrIDRef");
1133
+ if (!prId) return null;
1134
+ const ref = sm.paraHeadings.get(prId);
1135
+ if (!ref) return null;
1136
+ if (ref.type === "BULLET") {
1137
+ const char = sm.bullets.get(ref.idRef);
1138
+ return char ? { prefix: char } : null;
1433
1139
  }
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
- }
1140
+ const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
1141
+ const level = Math.min(ref.level + 1, 10);
1142
+ const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
1143
+ const numDef = sm.numberings.get(numId);
1144
+ if (!numDef) return headingLevel ? { headingLevel } : null;
1145
+ let counters = ctx.shared.numState.get(numId);
1146
+ if (!counters) {
1147
+ counters = new Array(11).fill(0);
1148
+ ctx.shared.numState.set(numId, counters);
1453
1149
  }
1150
+ const head = numDef.heads.get(level);
1151
+ counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
1152
+ for (let l = level + 1; l <= 10; l++) counters[l] = 0;
1153
+ const fmtText = head?.text?.trim() || `^${level}.`;
1154
+ const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1155
+ const lv = parseInt(d, 10);
1156
+ const refHead = numDef.heads.get(lv);
1157
+ const n = counters[lv] || refHead?.start || 1;
1158
+ return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1159
+ });
1160
+ return { prefix, headingLevel };
1454
1161
  }
1162
+
1163
+ // src/hwpx/table-build.ts
1455
1164
  function buildTableWithCellMeta(state) {
1456
1165
  const table = buildTable(state.rows);
1457
1166
  if (state.caption) table.caption = state.caption;
@@ -1531,6 +1240,8 @@ function completeTable(newTable, tableStack, blocks, ctx) {
1531
1240
  }
1532
1241
  return parentTable;
1533
1242
  }
1243
+
1244
+ // src/hwpx/section-walker.ts
1534
1245
  function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1535
1246
  const parser = createXmlParser(warnings);
1536
1247
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
@@ -2064,49 +1775,357 @@ function extractParagraphInfo(para, styleMap, ctx) {
2064
1775
  break;
2065
1776
  }
2066
1777
  }
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;
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;
1796
+ }
1797
+ }
1798
+ return { text: cleanText, href, footnote, style };
1799
+ }
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";
1824
+ }
1825
+ }
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";
1836
+ }
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
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ }
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
+ }
1878
+ }
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;
1898
+ }
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);
1902
+ }
1903
+ if (!img) {
1904
+ block.type = "paragraph";
1905
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
1906
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\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(`![image](${ref})`, `![image](${img.filename})`);
1912
+ }
1913
+ return images;
1914
+ }
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++;
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;
2085
1982
  }
2086
1983
  }
2087
- return { text: cleanText, href, footnote, style };
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 };
2088
1988
  }
2089
- function findChildByLocalName(parent, name) {
2090
- const children = parent.childNodes;
2091
- if (!children) return null;
2092
- for (let i = 0; i < children.length; i++) {
2093
- const ch = children[i];
2094
- if (ch.nodeType !== 1) continue;
2095
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
2096
- if (tag === name) return ch;
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;
2097
1998
  }
2098
- return null;
1999
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2000
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2099
2001
  }
2100
- function extractTextFromNode(node) {
2101
- let result = "";
2102
- const children = node.childNodes;
2103
- if (!children) return result;
2104
- for (let i = 0; i < children.length; i++) {
2105
- const child = children[i];
2106
- if (child.nodeType === 3) result += child.textContent || "";
2107
- else if (child.nodeType === 1) result += extractTextFromNode(child);
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);
2108
2013
  }
2109
- return result.trim();
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);
2019
+ }
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)");
2036
+ }
2037
+ parseDublinCoreMetadata(xml, metadata);
2038
+ if (metadata.title || metadata.author) return;
2039
+ }
2040
+ } catch {
2041
+ }
2042
+ }
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);
2065
+ }
2066
+ }
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);
2076
+ }
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 JSZip3 from "jszip";
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 JSZip3.loadAsync(buffer);
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 JSZip4 from "jszip";
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 JSZip4.loadAsync(buffer);
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 JSZip5 from "jszip";
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 JSZip5.loadAsync(hwpxBuffer);
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 JSZip6 from "jszip";
20254
+ import JSZip7 from "jszip";
20236
20255
 
20237
20256
  // src/hwpx/text-metrics.ts
20238
20257
  var ASCII_W = [
@@ -21068,7 +21087,7 @@ async function markdownToHwpx(markdown, options) {
21068
21087
  const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
21069
21088
  const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
21070
21089
  const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
21071
- const zip = new JSZip6();
21090
+ const zip = new JSZip7();
21072
21091
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
21073
21092
  zip.file("META-INF/container.xml", generateContainerXml());
21074
21093
  zip.file("Contents/content.hpf", generateManifest());
@@ -21895,7 +21914,7 @@ function diffTableCells(a, b) {
21895
21914
  }
21896
21915
 
21897
21916
  // src/roundtrip/patcher.ts
21898
- import JSZip7 from "jszip";
21917
+ import JSZip8 from "jszip";
21899
21918
 
21900
21919
  // src/roundtrip/table-rows.ts
21901
21920
  var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
@@ -22533,7 +22552,7 @@ async function patchHwpx(original, editedMarkdown, options) {
22533
22552
  }
22534
22553
  let zip;
22535
22554
  try {
22536
- zip = await JSZip7.loadAsync(original);
22555
+ zip = await JSZip8.loadAsync(original);
22537
22556
  } catch {
22538
22557
  return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
22539
22558
  }
@@ -23900,10 +23919,10 @@ function stageParaPatch(scan, para, newPlain, skip) {
23900
23919
  }
23901
23920
 
23902
23921
  // src/roundtrip/session.ts
23903
- import JSZip8 from "jszip";
23922
+ import JSZip9 from "jszip";
23904
23923
  async function buildState(bytes) {
23905
23924
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
23906
- const zip = await JSZip8.loadAsync(bytes);
23925
+ const zip = await JSZip9.loadAsync(bytes);
23907
23926
  const sectionPaths = await resolveSectionEntryNames(zip);
23908
23927
  if (sectionPaths.length === 0) {
23909
23928
  throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -24484,7 +24503,7 @@ async function parseHwp(buffer, options) {
24484
24503
  async function parsePdf(buffer, options) {
24485
24504
  let parsePdfDocument;
24486
24505
  try {
24487
- const mod = await import("./parser-DCK42RMA.js");
24506
+ const mod = await import("./parser-XEDROIM7.js");
24488
24507
  parsePdfDocument = mod.parsePdfDocument;
24489
24508
  } catch {
24490
24509
  return {