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.
Files changed (32) hide show
  1. package/README.md +11 -1
  2. package/dist/{-ATVQYFSW.js → -FEHSMPVO.js} +3 -3
  3. package/dist/{chunk-QV25HMU7.js → chunk-553VTUVP.js} +2 -2
  4. package/dist/{chunk-SLKF72QF.js → chunk-DP37KF2X.js} +1985 -1954
  5. package/dist/chunk-DP37KF2X.js.map +1 -0
  6. package/dist/{chunk-3R3YK7EM.js → chunk-JHZUFBUV.js} +2 -2
  7. package/dist/{chunk-ITJIALN5.cjs → chunk-YBPNKFJW.cjs} +2 -2
  8. package/dist/{chunk-ITJIALN5.cjs.map → chunk-YBPNKFJW.cjs.map} +1 -1
  9. package/dist/cli.js +4 -4
  10. package/dist/index.cjs +2059 -2028
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +17 -8
  13. package/dist/index.d.ts +17 -8
  14. package/dist/index.js +1974 -1943
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp.js +3 -3
  17. package/dist/{parser-7G5F7PT2.js → parser-KNQDRLZQ.js} +1930 -1863
  18. package/dist/parser-KNQDRLZQ.js.map +1 -0
  19. package/dist/{parser-DCK42RMA.js → parser-NR2TYGO3.js} +1930 -1863
  20. package/dist/parser-NR2TYGO3.js.map +1 -0
  21. package/dist/{parser-GUSJH44K.cjs → parser-NS4ZPD7B.cjs} +1933 -1864
  22. package/dist/parser-NS4ZPD7B.cjs.map +1 -0
  23. package/dist/{watch-GVZESOCE.js → watch-XCWADLPU.js} +3 -3
  24. package/package.json +2 -2
  25. package/dist/chunk-SLKF72QF.js.map +0 -1
  26. package/dist/parser-7G5F7PT2.js.map +0 -1
  27. package/dist/parser-DCK42RMA.js.map +0 -1
  28. package/dist/parser-GUSJH44K.cjs.map +0 -1
  29. /package/dist/{-ATVQYFSW.js.map → -FEHSMPVO.js.map} +0 -0
  30. /package/dist/{chunk-QV25HMU7.js.map → chunk-553VTUVP.js.map} +0 -0
  31. /package/dist/{chunk-3R3YK7EM.js.map → chunk-JHZUFBUV.js.map} +0 -0
  32. /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-QV25HMU7.js";
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 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,173 +1066,7 @@ 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
- }
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
- 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);
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 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" });
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
- 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 };
1221
+ return table;
1151
1222
  }
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 })));
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
- if (footers.length > 0) {
1158
- blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
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
- 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";
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 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
- }
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
- 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;
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
- 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;
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
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
1260
- resolved.set(ref, img);
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(`![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
1396
  }
1272
- return images;
1273
1397
  }
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 {
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 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;
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" : "") + `![image](${b.text})`;
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 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;
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
- 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;
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
- 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 };
1519
+ return null;
1382
1520
  }
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;
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 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);
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 Array.from(idToHref.values()).sort(compareSectionPaths);
1564
+ return void 0;
1417
1565
  }
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
- }
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 buildTableWithCellMeta(state) {
1456
- const table = buildTable(state.rows);
1457
- if (state.caption) table.caption = state.caption;
1458
- const anchors = [];
1459
- {
1460
- const covered = /* @__PURE__ */ new Set();
1461
- for (let r = 0; r < table.rows; r++) {
1462
- for (let c = 0; c < table.cols; c++) {
1463
- if (covered.has(`${r},${c}`)) continue;
1464
- const cell = table.cells[r]?.[c];
1465
- if (!cell) continue;
1466
- for (let dr = 0; dr < cell.rowSpan; dr++) {
1467
- for (let dc = 0; dc < cell.colSpan; dc++) {
1468
- if (dr === 0 && dc === 0) continue;
1469
- if (r + dr < table.rows && c + dc < table.cols) covered.add(`${r + dr},${c + dc}`);
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 srcCount = state.rows.reduce((s, r) => s + r.length, 0);
1478
- const ordinalReliable = anchors.length === srcCount;
1479
- const claimed = /* @__PURE__ */ new Set();
1480
- let flatIdx = -1;
1481
- for (const row of state.rows) {
1482
- for (const src of row) {
1483
- flatIdx++;
1484
- const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
1485
- if (!needsBlocks && !src.isHeader) continue;
1486
- let target;
1487
- const trimmed = src.text.trim();
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 (!target && ordinalReliable) {
1503
- const cand = anchors[flatIdx];
1504
- if (cand && !claimed.has(cand)) target = cand;
1505
- }
1506
- if (!target) continue;
1507
- claimed.add(target);
1508
- if (needsBlocks) target.blocks = src.blocks;
1509
- if (src.isHeader) target.isHeader = true;
1510
- }
1511
- }
1512
- return table;
1513
- }
1514
- function completeTable(newTable, tableStack, blocks, ctx) {
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
- break;
1613
- case "cellAddr":
1614
- if (tableCtx?.cell) {
1615
- const ca = parseInt(el.getAttribute("colAddr") || "", 10);
1616
- const ra = parseInt(el.getAttribute("rowAddr") || "", 10);
1617
- if (!isNaN(ca)) tableCtx.cell.colAddr = ca;
1618
- if (!isNaN(ra)) tableCtx.cell.rowAddr = ra;
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
- break;
1621
- case "cellSpan":
1622
- if (tableCtx?.cell) {
1623
- const rawCs = parseInt(el.getAttribute("colSpan") || "1", 10);
1624
- const cs = isNaN(rawCs) ? 1 : rawCs;
1625
- const rawRs = parseInt(el.getAttribute("rowSpan") || "1", 10);
1626
- const rs = isNaN(rawRs) ? 1 : rawRs;
1627
- tableCtx.cell.colSpan = clampSpan(cs, MAX_COLS);
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
- break;
1631
- case "p": {
1632
- const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
1633
- let text = rawText;
1634
- let headingLevel;
1635
- if (text) {
1636
- const ph = resolveParaHeading(el, ctx);
1637
- if (ph?.prefix) text = ph.prefix + " " + text;
1638
- headingLevel = ph?.headingLevel;
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
- if (text) {
1641
- if (tableCtx?.cell) {
1642
- const cell = tableCtx.cell;
1643
- if (footnote) text += ` (\uC8FC: ${footnote})`;
1644
- cell.text += (cell.text ? "\n" : "") + text;
1645
- (cell.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1646
- } else if (!tableCtx) {
1647
- const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1648
- if (headingLevel) block.level = headingLevel;
1649
- if (style) block.style = style;
1650
- if (href) block.href = href;
1651
- if (footnote) block.footnoteText = footnote;
1652
- blocks.push(block);
1653
- } else {
1654
- blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
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
- tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1658
- break;
1659
- }
1660
- // 이미지/그림/글상자 이미지·텍스트·캡션 병행 추출
1661
- case "pic":
1662
- case "shape":
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
- break;
1773
+ default:
1774
+ walk(child);
1775
+ break;
1680
1776
  }
1681
- default:
1682
- walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1683
- break;
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
- function handleShape(el, sink, ctx) {
1688
- const imgRef = extractImageRef(el);
1689
- const drawTextChild = findDescendant(el, "drawText");
1690
- if (imgRef) {
1691
- const block = { type: "image", text: imgRef, pageNumber: ctx.sectionNum };
1692
- const alt = userShapeComment(el);
1693
- if (alt) block.footnoteText = alt;
1694
- sink.push(block);
1695
- }
1696
- if (drawTextChild) {
1697
- extractDrawTextBlocks(drawTextChild, sink, ctx);
1698
- }
1699
- const capEl = findChildByLocalName(el, "caption");
1700
- if (capEl) {
1701
- const capText = collectSubListText(capEl, ctx);
1702
- if (capText) sink.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
1703
- }
1704
- if (!imgRef && !drawTextChild && ctx.warnings && ctx.sectionNum) {
1705
- const localTag = (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
1706
- ctx.warnings.push({ page: ctx.sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
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 userShapeComment(el) {
1710
- const commentEl = findChildByLocalName(el, "shapeComment");
1711
- if (!commentEl) return void 0;
1712
- const text = extractTextFromNode(commentEl);
1713
- if (!text) return void 0;
1714
- if (/^그림입니다/.test(text)) return void 0;
1715
- if (/^(?:모서리가 둥근 |둥근 )?[^\n]{1,20}입니다\.?$/.test(text)) return void 0;
1716
- return text;
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 mergeBlocksIntoCell(cell, sink) {
1719
- for (const b of sink) {
1720
- if ((b.type === "paragraph" || b.type === "heading") && b.text) {
1721
- cell.text += (cell.text ? "\n" : "") + b.text;
1722
- (cell.blocks ??= []).push(b);
1723
- } else if (b.type === "image" || b.type === "table") {
1724
- if (b.type === "image" && b.text) {
1725
- cell.text += (cell.text ? "\n" : "") + `![image](${b.text})`;
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 walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1755
- if (depth > MAX_XML_DEPTH) return tableCtx;
1756
- const children = node.childNodes;
1757
- if (!children) return tableCtx;
1758
- const walkChildren = (parent, d) => {
1759
- if (d > MAX_XML_DEPTH) return;
1760
- const kids2 = parent.childNodes;
1761
- if (!kids2) return;
1762
- for (let i = 0; i < kids2.length; i++) {
1763
- const el = kids2[i];
1764
- if (el.nodeType !== 1) continue;
1765
- const tag = el.tagName || el.localName || "";
1766
- const localTag = tag.replace(/^[^:]+:/, "");
1767
- if (localTag === "tbl") {
1768
- if (tableCtx) tableStack.push(tableCtx);
1769
- const newTable = { rows: [], currentRow: [], cell: null };
1770
- walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
1771
- tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1772
- } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
1773
- if (tableCtx?.cell) {
1774
- const sink = [];
1775
- handleShape(el, sink, ctx);
1776
- mergeBlocksIntoCell(tableCtx.cell, sink);
1777
- } else {
1778
- handleShape(el, blocks, ctx);
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
- } else if (localTag === "drawText") {
1781
- if (tableCtx?.cell) {
1782
- const sink = [];
1783
- extractDrawTextBlocks(el, sink, ctx);
1784
- mergeBlocksIntoCell(tableCtx.cell, sink);
1785
- } else {
1786
- extractDrawTextBlocks(el, blocks, ctx);
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
- walkChildren(node, depth);
1794
- return tableCtx;
1795
- }
1796
- function findDescendant(node, targetTag, depth = 0) {
1797
- if (depth > 5) return null;
1798
- const children = node.childNodes;
1799
- if (!children) return null;
1800
- for (let i = 0; i < children.length; i++) {
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(`![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})`);
1807
1912
  }
1808
- return null;
1913
+ return images;
1809
1914
  }
1810
- function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
1811
- const children = drawTextNode.childNodes;
1812
- if (!children) return;
1813
- for (let i = 0; i < children.length; i++) {
1814
- const child = children[i];
1815
- if (child.nodeType !== 1) continue;
1816
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1817
- if (tag === "subList" || tag === "p" || tag === "para") {
1818
- if (tag === "subList") {
1819
- extractDrawTextBlocks(child, blocks, ctx);
1820
- } else {
1821
- const info = extractParagraphInfo(child, ctx.styleMap, ctx);
1822
- let text = info.text.trim();
1823
- if (text) {
1824
- const ph = resolveParaHeading(child, ctx);
1825
- if (ph?.prefix) text = ph.prefix + " " + text;
1826
- const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
1827
- if (info.href) block.href = info.href;
1828
- if (info.footnote) block.footnoteText = info.footnote;
1829
- blocks.push(block);
1830
- }
1831
- walkParagraphChildren(child, blocks, null, [], ctx);
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 extractHyperlinkHref(fieldBegin) {
1837
- if ((fieldBegin.getAttribute("type") || "").toUpperCase() !== "HYPERLINK") return void 0;
1838
- const params = findChildByLocalName(fieldBegin, "parameters");
1839
- if (!params) return void 0;
1840
- const children = params.childNodes;
1841
- if (!children) return void 0;
1842
- for (let i = 0; i < children.length; i++) {
1843
- const ch = children[i];
1844
- if (ch.nodeType !== 1) continue;
1845
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1846
- if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
1847
- let url = (ch.textContent || "").trim();
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 extractParagraphInfo(para, styleMap, ctx) {
1859
- let text = "";
1860
- let href;
1861
- let footnote;
1862
- let charPrId;
1863
- const handleCtrl = (ctrlEl) => {
1864
- const kids2 = ctrlEl.childNodes;
1865
- if (!kids2) return;
1866
- for (let j = 0; j < kids2.length; j++) {
1867
- const k = kids2[j];
1868
- if (k.nodeType !== 1) continue;
1869
- const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
1870
- switch (ktag) {
1871
- // 머리말/꼬리말 문서당 1회 수집, 본문 앞/뒤 배치
1872
- case "header":
1873
- case "footer": {
1874
- if (!ctx) break;
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
- const walk = (node) => {
1935
- const children = node.childNodes;
1936
- if (!children) return;
1937
- for (let i = 0; i < children.length; i++) {
1938
- const child = children[i];
1939
- if (child.nodeType === 3) {
1940
- const t = child.textContent || "";
1941
- if (isInDeletedRange(ctx)) {
1942
- if (t && ctx && !ctx.shared.track.warned) {
1943
- ctx.shared.track.warned = true;
1944
- ctx.warnings?.push({ page: ctx.sectionNum, message: "\uBCC0\uACBD\uCD94\uC801 \uC0AD\uC81C \uD14D\uC2A4\uD2B8 \uCD9C\uB825 \uC81C\uC678", code: "HIDDEN_TEXT_FILTERED" });
1945
- }
1946
- } else {
1947
- text += t;
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 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;
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
- 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);
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
- return result.trim();
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 = [
@@ -20603,53 +20622,112 @@ function mmToHwpunit(mm) {
20603
20622
  return Math.round(mm * 7200 / 25.4);
20604
20623
  }
20605
20624
 
20606
- // src/diff/text-diff.ts
20607
- function similarity(a, b) {
20608
- if (a === b) return 1;
20609
- if (!a || !b) return 0;
20610
- const maxLen = Math.max(a.length, b.length);
20611
- if (maxLen === 0) return 1;
20612
- return 1 - levenshtein(a, b) / maxLen;
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 normalizedSimilarity(a, b) {
20615
- return similarity(normalize(a), normalize(b));
20667
+ function escapeXml(text) {
20668
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20616
20669
  }
20617
- function normalize(s) {
20618
- return s.replace(/\s+/g, " ").trim();
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
- var MAX_LEVENSHTEIN_LEN = 1e4;
20621
- function levenshtein(a, b) {
20622
- if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
20623
- const sampleLen = Math.min(500, a.length, b.length);
20624
- let diffs = 0;
20625
- for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
20626
- const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
20627
- return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
20628
- }
20629
- if (a.length > b.length) [a, b] = [b, a];
20630
- const m = a.length;
20631
- const n = b.length;
20632
- let prev = Array.from({ length: m + 1 }, (_, i) => i);
20633
- let curr = new Array(m + 1);
20634
- for (let j = 1; j <= n; j++) {
20635
- curr[0] = j;
20636
- for (let i = 1; i <= m; i++) {
20637
- if (a[i - 1] === b[j - 1]) {
20638
- curr[i] = prev[i - 1];
20639
- } else {
20640
- curr[i] = 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
20641
- }
20642
- }
20643
- ;
20644
- [prev, curr] = [curr, prev];
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 prev[m];
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 units = [];
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
- if (line.trim().startsWith("<table>")) {
20661
- const collected2 = [];
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
- collected2.push(l);
20666
- depth += (l.match(/<table>/g) || []).length;
20667
- depth -= (l.match(/<\/table>/g) || []).length;
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
- units.push({ kind: "html-table", raw: collected2.join("\n"), lines: collected2 });
20774
+ blocks.push({ type: "html_table", text: htmlLines.join("\n") });
20672
20775
  continue;
20673
20776
  }
20674
20777
  if (line.trimStart().startsWith("|")) {
20675
- const collected2 = [];
20778
+ const tableRows = [];
20676
20779
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20677
- collected2.push(lines[i]);
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
- units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
20789
+ if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
20681
20790
  continue;
20682
20791
  }
20683
- if (/^-{3,}\s*$/.test(line.trim())) {
20684
- units.push({ kind: "separator", raw: line.trim(), lines: [line.trim()] });
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
- if (/^!\[image\]\([^)]*\)\s*$/.test(line.trim())) {
20689
- units.push({ kind: "image", raw: line.trim(), lines: [line.trim()] });
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
- const collected = [];
20694
- while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("|") && !lines[i].trim().startsWith("<table>")) {
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 units;
20814
+ return blocks;
20701
20815
  }
20702
- function alignUnits(a, b) {
20703
- const m = a.length, n = b.length;
20704
- if (m * n > 4e6) {
20705
- const result2 = [];
20706
- let pre = 0;
20707
- while (pre < m && pre < n && a[pre] === b[pre]) {
20708
- result2.push([pre, pre]);
20709
- pre++;
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
- let suf = 0;
20712
- while (suf < m - pre && suf < n - pre && a[m - 1 - suf] === b[n - 1 - suf]) suf++;
20713
- const aMid = m - pre - suf, bMid = n - pre - suf;
20714
- if (aMid === bMid) {
20715
- for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, pre + i2]);
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
- for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, null]);
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
- return result;
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
- const uniqueRows = [];
20830
- let pendingLabelRow = null;
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
- if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
20849
- return uniqueRows.length > 0 ? uniqueRows : null;
20846
+ return spans;
20850
20847
  }
20851
- function parseGfmTable(lines) {
20852
- const rows = [];
20853
- for (const line of lines) {
20854
- const trimmed = line.trim();
20855
- if (!trimmed.startsWith("|")) continue;
20856
- const cells = trimmed.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim());
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 unescapeGfmCell(text) {
20864
- return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\~/g, "~");
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 replicateCellInnerHtml(cell) {
20867
- if (cell.blocks?.length) {
20868
- return cell.blocks.map((b) => {
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
- return sanitizeText(cell.text).replace(/\n/g, "<br>");
20867
+ const runs = generateRuns(text, charPrId, mapCharId);
20868
+ return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
20879
20869
  }
20880
- function replicateTableToHtml(table) {
20881
- const rows = replicateHtmlTable(table);
20882
- const lines = ["<table>"];
20883
- for (let r = 0; r < rows.length; r++) {
20884
- const tag = rows[r].tag;
20885
- const rowHtml = rows[r].cells.map((cell) => {
20886
- const attrs = [];
20887
- if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
20888
- if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
20889
- const attrStr = attrs.length ? " " + attrs.join(" ") : "";
20890
- return `<${tag}${attrStr}>${cell.inner}</${tag}>`;
20891
- });
20892
- if (rowHtml.length) lines.push(`<tr>${rowHtml.join("")}</tr>`);
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
- lines.push("</table>");
20895
- return lines.join("\n");
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 replicateHtmlTable(table) {
20898
- const { cells, rows: numRows, cols: numCols } = table;
20899
- const skip = /* @__PURE__ */ new Set();
20900
- const result = [];
20901
- for (let r = 0; r < numRows; r++) {
20902
- const tag = r === 0 ? "th" : "td";
20903
- const rowCells = [];
20904
- for (let c = 0; c < numCols; c++) {
20905
- if (skip.has(`${r},${c}`)) continue;
20906
- const cell = cells[r]?.[c];
20907
- if (!cell) continue;
20908
- for (let dr = 0; dr < cell.rowSpan; dr++) {
20909
- for (let dc = 0; dc < cell.colSpan; dc++) {
20910
- if (dr === 0 && dc === 0) continue;
20911
- if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
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
- return result;
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
- function parseHtmlTable(raw) {
20927
- const re = /<(\/?)(table|tr|td|th)((?:"[^"]*"|'[^']*'|[^>"'])*?)>/gi;
20928
- let depth = 0;
20929
- let currentRow = null;
20930
- let cellStart = -1;
20931
- let cellInfo = null;
20932
- const rows = [];
20933
- let m;
20934
- while ((m = re.exec(raw)) !== null) {
20935
- const isClose = m[1] === "/";
20936
- const tag = m[2].toLowerCase();
20937
- const attrs = m[3] || "";
20938
- if (tag === "table") {
20939
- depth += isClose ? -1 : 1;
20940
- if (depth < 0) return null;
20941
- continue;
20942
- }
20943
- if (depth !== 1) continue;
20944
- if (tag === "tr") {
20945
- if (!isClose) currentRow = [];
20946
- else if (currentRow) {
20947
- rows.push({ tag: rows.length === 0 ? "th" : "td", cells: currentRow });
20948
- currentRow = null;
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
- if (!isClose) {
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
- if (depth !== 0) return null;
20964
- return rows;
21091
+ return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
20965
21092
  }
20966
- var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-9a-zA-Z가-힣]{1,6}[)\]][.:]?|[ⅰ-ⅹⅠ-Ⅹ①-⑮][.)\]:]?)$/u;
20967
- function htmlCellInnerToLines(inner) {
20968
- let hadNonText = false;
20969
- let work = inner;
20970
- if (/<table[\s>]/i.test(work)) {
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 extractTopLevelTables(html) {
20982
- const result = [];
20983
- let depth = 0;
20984
- let start = -1;
20985
- const re = /<(\/?)table(?:[\s>]|>)/gi;
20986
- let m;
20987
- while ((m = re.exec(html)) !== null) {
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
- return result;
21001
- }
21002
- function removeNestedTables(html) {
21003
- let result = "";
21004
- let depth = 0;
21005
- const re = /<(\/?)table(?:[\s>]|>)/gi;
21006
- let last = 0;
21007
- let m;
21008
- while ((m = re.exec(html)) !== null) {
21009
- if (m[1] !== "/") {
21010
- if (depth === 0) result += html.slice(last, m.index);
21011
- depth++;
21012
- } else {
21013
- depth--;
21014
- if (depth === 0) last = m.index + m[0].length;
21015
- if (depth < 0) depth = 0;
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/hwpx/generator.ts
21023
- var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
21024
- var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
21025
- var NS_HEAD = "http://www.hancom.co.kr/hwpml/2011/head";
21026
- var NS_CORE = "http://www.hancom.co.kr/hwpml/2011/core";
21027
- var NS_OPF = "http://www.idpf.org/2007/opf/";
21028
- var NS_HPF = "http://www.hancom.co.kr/schema/2011/hpf";
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
- async function markdownToHwpx(markdown, options) {
21065
- const theme = resolveTheme(options?.theme);
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 buildPrvText(blocks) {
21081
- const lines = [];
21082
- let bytes = 0;
21083
- for (const b of blocks) {
21084
- let text = b.text || (b.rows ? b.rows.map((r) => r.join(" ")).join("\n") : "");
21085
- if (b.type === "html_table") text = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
21086
- if (!text) continue;
21087
- lines.push(text);
21088
- bytes += text.length * 3;
21089
- if (bytes > 1024) break;
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 lines.join("\n").slice(0, 1024);
21176
+ return prev[m];
21092
21177
  }
21093
- function parseMarkdownToBlocks(md2) {
21178
+
21179
+ // src/roundtrip/markdown-units.ts
21180
+ function splitMarkdownUnits(md2) {
21094
21181
  const lines = md2.split("\n");
21095
- const blocks = [];
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
- const fenceMatch = line.match(/^(`{3,}|~{3,})(.*)$/);
21104
- if (fenceMatch) {
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
- htmlLines.push(l);
21134
- depth += (l.match(/<table[\s>]/gi) ?? []).length;
21135
- depth -= (l.match(/<\/table>/gi) ?? []).length;
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
- blocks.push({ type: "html_table", text: htmlLines.join("\n") });
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 tableRows = [];
21205
+ const collected2 = [];
21144
21206
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
21145
- const row = lines[i];
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
- if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
21210
+ units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
21155
21211
  continue;
21156
21212
  }
21157
- if (line.trimStart().startsWith("> ")) {
21158
- const quoteLines = [];
21159
- while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
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
- const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
21169
- if (listMatch) {
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
- blocks.push({ type: "paragraph", text: line.trim() });
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 blocks;
21230
+ return units;
21180
21231
  }
21181
- function parseInlineMarkdown(text) {
21182
- text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
21183
- text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
21184
- text = text.replace(/~~([^~]+)~~/g, "$1");
21185
- const spans = [];
21186
- const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
21187
- let lastIdx = 0;
21188
- for (const match of text.matchAll(regex)) {
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
- const raw = match[0];
21194
- if (raw.startsWith("`")) {
21195
- spans.push({ text: raw.slice(1, -1), bold: false, italic: false, code: true });
21196
- } else if (raw.startsWith("***") || raw.startsWith("___")) {
21197
- spans.push({ text: raw.slice(3, -3), bold: true, italic: true, code: false });
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
- spans.push({ text: raw.slice(1, -1), bold: false, italic: true, code: false });
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
- lastIdx = idx + raw.length;
21250
+ for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
21251
+ return result2;
21204
21252
  }
21205
- if (lastIdx < text.length) {
21206
- spans.push({ text: text.slice(lastIdx), bold: false, italic: false, code: false });
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
- if (spans.length === 0) {
21209
- spans.push({ text, bold: false, italic: false, code: false });
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
- return spans;
21212
- }
21213
- function spanToCharPrId(span) {
21214
- if (span.code) return CHAR_CODE;
21215
- if (span.bold && span.italic) return CHAR_BOLD_ITALIC;
21216
- if (span.bold) return CHAR_BOLD;
21217
- if (span.italic) return CHAR_ITALIC;
21218
- return CHAR_NORMAL;
21219
- }
21220
- function escapeXml(text) {
21221
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21222
- }
21223
- function generateRuns(text, defaultCharPr = CHAR_NORMAL, mapCharId) {
21224
- const spans = parseInlineMarkdown(text);
21225
- return spans.map((span) => {
21226
- let charId = span.code || span.bold || span.italic ? spanToCharPrId(span) : defaultCharPr;
21227
- if (mapCharId) charId = mapCharId(charId);
21228
- return `<hp:run charPrIDRef="${charId}"><hp:t>${escapeXml(span.text)}</hp:t></hp:run>`;
21229
- }).join("");
21230
- }
21231
- function generateParagraph(text, paraPrId = PARA_NORMAL, charPrId = CHAR_NORMAL, mapCharId) {
21232
- if (paraPrId === PARA_CODE) {
21233
- return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_CODE}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
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
- const runs = generateRuns(text, charPrId, mapCharId);
21236
- return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
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 headingCharPrId(level) {
21245
- if (level === 1) return CHAR_H1;
21246
- if (level === 2) return CHAR_H2;
21247
- if (level === 3) return CHAR_H3;
21248
- return CHAR_H4;
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 generateContainerXml() {
21251
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
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
- function generateManifest() {
21259
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
21260
- <opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
21261
- <opf:manifest>
21262
- <opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
21263
- <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>
21264
- </opf:manifest>
21265
- <opf:spine>
21266
- <opf:itemref idref="header" linear="no"/>
21267
- <opf:itemref idref="section0" linear="yes"/>
21268
- </opf:spine>
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 charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_COLOR, ratioPct = 100) {
21272
- const boldAttr = bold ? ` bold="1"` : "";
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 paraPr(id, opts = {}) {
21284
- const { align = "JUSTIFY", spaceBefore = 0, spaceAfter = 0, lineSpacing = 160, indent = 0, left = 0, keepWord = false } = opts;
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
- var GONGMUN_LIST_BASE = 8;
21298
- var GONGMUN_LIST_LEVELS = 8;
21299
- var GONGMUN_CENTER = GONGMUN_LIST_BASE + GONGMUN_LIST_LEVELS;
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 computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21306
- const minRatio = gongmun.autoFitMinRatio;
21307
- if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
21308
- const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
21309
- const ratioByBlock = /* @__PURE__ */ new Map();
21310
- const variants = [];
21311
- for (let i = 0; i < blocks.length; i++) {
21312
- const block = blocks[i];
21313
- let text;
21314
- let firstW;
21315
- let contW;
21316
- if (block.type === "list_item" && gongmunList.has(i)) {
21317
- const { marker, depth } = gongmunList.get(i);
21318
- const content = plainRenderText(block.text || "");
21319
- text = marker ? `${marker} ${content}` : content;
21320
- const { left, indent } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering);
21321
- firstW = pageW - left - Math.max(indent, 0);
21322
- contW = pageW - left - Math.max(-indent, 0);
21323
- } else if (block.type === "paragraph") {
21324
- const raw = (block.text || "").trim();
21325
- if (/^<center>[\s\S]*<\/center>$/i.test(raw)) continue;
21326
- text = plainRenderText(raw);
21327
- firstW = contW = pageW;
21328
- } else {
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 (!text) continue;
21332
- const r = fitRatioForFewerLines(text, firstW, contW, gongmun.bodyHeight, GONGMUN_BODY_RATIO, minRatio);
21333
- if (r === null) continue;
21334
- ratioByBlock.set(i, r);
21335
- if (!variants.includes(r)) variants.push(r);
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
- return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
21378
+ if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
21379
+ return uniqueRows.length > 0 ? uniqueRows : null;
21338
21380
  }
21339
- function variantMapper(fit, blockIdx) {
21340
- const r = fit.ratioByBlock.get(blockIdx);
21341
- if (r === void 0) return void 0;
21342
- const vi = fit.variants.indexOf(r);
21343
- return (id) => id >= 0 && id <= 3 ? CHAR_VARIANT_BASE + vi * 4 + id : id;
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 buildCharProperties(theme, gongmun, ratioVariants = []) {
21346
- let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
21347
- if (gongmun) {
21348
- body = gongmun.bodyHeight;
21349
- code = Math.max(body - 200, 900);
21350
- h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
21351
- h2 = 1600;
21352
- h3 = body;
21353
- h4 = Math.max(body - 100, 1300);
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
- const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
21356
- const rows = [
21357
- charPr(0, body, false, false, 0, theme.body, bodyRatio),
21358
- charPr(1, body, true, false, 0, theme.body, bodyRatio),
21359
- charPr(2, body, false, true, 0, theme.body, bodyRatio),
21360
- charPr(3, body, true, true, 0, theme.body, bodyRatio),
21361
- charPr(4, code, false, false, 1),
21362
- charPr(5, h1, true, false, 1, theme.h1),
21363
- charPr(6, h2, true, false, 1, theme.h2),
21364
- charPr(7, h3, true, false, 1, theme.h3),
21365
- charPr(8, h4, true, false, 1, theme.h4),
21366
- charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, 0, theme.tableHeader),
21367
- charPr(CHAR_QUOTE, body, false, true, 0, theme.quote)
21368
- ];
21369
- for (const r of ratioVariants) {
21370
- rows.push(
21371
- charPr(rows.length, body, false, false, 0, theme.body, r),
21372
- charPr(rows.length + 1, body, true, false, 0, theme.body, r),
21373
- charPr(rows.length + 2, body, false, true, 0, theme.body, r),
21374
- charPr(rows.length + 3, body, true, true, 0, theme.body, r)
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 `<hh:charProperties itemCnt="${rows.length}">
21378
- ${rows.join("\n")}
21379
- </hh:charProperties>`;
21493
+ if (depth !== 0) return null;
21494
+ return rows;
21380
21495
  }
21381
- function buildParaProperties(gongmun) {
21382
- if (!gongmun) {
21383
- const base2 = [
21384
- paraPr(0),
21385
- paraPr(1, { align: "LEFT", spaceBefore: 800, spaceAfter: 200, lineSpacing: 180 }),
21386
- paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: 170 }),
21387
- paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: 160 }),
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
- const ls = gongmun.lineSpacing;
21398
- const titleAlign = gongmun.centerTitle ? "CENTER" : "LEFT";
21399
- const base = [
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
- base.push(paraPr(GONGMUN_CENTER, { align: "CENTER", lineSpacing: ls, keepWord: true }));
21415
- return `<hh:paraProperties itemCnt="${base.length}">
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 generateHeaderXml(theme, gongmun, ratioVariants = []) {
21420
- const bodyFace = gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5";
21421
- const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants);
21422
- const paraPropsXml = buildParaProperties(gongmun);
21423
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
21424
- <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
21425
- <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
21426
- <hh:refList>
21427
- <hh:fontfaces itemCnt="7">
21428
- <hh:fontface lang="HANGUL" fontCnt="3">
21429
- <hh:font id="0" face="${bodyFace}" type="TTF" isEmbedded="0">
21430
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21431
- </hh:font>
21432
- <hh:font id="1" face="\uD568\uCD08\uB86C\uB3CB\uC6C0" type="TTF" isEmbedded="0">
21433
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21434
- </hh:font>
21435
- <hh:font id="2" face="HY\uACAC\uACE0\uB515" type="TTF" isEmbedded="0">
21436
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21437
- </hh:font>
21438
- </hh:fontface>
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 generateSecPr(gongmun) {
21507
- const m = gongmun ? {
21508
- top: mmToHwpunit(gongmun.margins.top),
21509
- bottom: mmToHwpunit(gongmun.margins.bottom),
21510
- left: mmToHwpunit(gongmun.margins.left),
21511
- right: mmToHwpunit(gongmun.margins.right),
21512
- header: 0,
21513
- footer: 0
21514
- } : { top: 8504, bottom: 4252, left: 5670, right: 4252, header: 2835, footer: 2835 };
21515
- 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>`;
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
- function precomputeGongmunList(blocks, gongmun) {
21607
- const result = /* @__PURE__ */ new Map();
21608
- let i = 0;
21609
- while (i < blocks.length) {
21610
- if (blocks[i].type !== "list_item") {
21611
- i++;
21612
- continue;
21613
- }
21614
- const run = [];
21615
- while (i < blocks.length) {
21616
- const t = blocks[i].type;
21617
- if (t === "list_item") {
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 JSZip7 from "jszip";
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 JSZip7.loadAsync(original);
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 JSZip8 from "jszip";
23934
+ import JSZip9 from "jszip";
23904
23935
  async function buildState(bytes) {
23905
23936
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
23906
- const zip = await JSZip8.loadAsync(bytes);
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-DCK42RMA.js");
24518
+ const mod = await import("./parser-NR2TYGO3.js");
24488
24519
  parsePdfDocument = mod.parsePdfDocument;
24489
24520
  } catch {
24490
24521
  return {